-
Notifications
You must be signed in to change notification settings - Fork 55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Possible BUG but Please help to understand how Webview2 should work with WPF MVVM concepts. #1136
Comments
Hey @darbid - You are right that not all of our properties are DependencyProperties and so you may run into limitations. Is your UserControl that wraps the WebView2 not able to create a static or shared Environment that is used by all instances? You could also look at using the CoreWebView2CreationProperties class to set the properties that are used to create the Environment - they wouldn't all use the exact same Environment object, but the effect would be the same (shared browser processes, etc.). For the Source property - it's currently By Design that it doesn't accept null (although maybe we should change that depending on this scenario). Can you share what the stack is when you hit the NotImplementedException? Why is it being set to null? |
After initially posting my issue I had that thought. So am I correct in assuming that if both old and new windows are set up with the same environment, eg CreateAsync is created for both with the same code,
To be honest this is at the limit of my understanding but am willing to learn. I first turned off "Just My Code" so I could see the WebView2 exception. No surprises this is in WebView2.cs at SourcePropertyChanged where the new value is checked for null. If I understand the call stack when the datacontext changes then context elements are being disconnected. I think that the error occurs when the WebView2 as contextelement is disconnected and it sets the dependency property to null. This is where the datacontext changes and interestingly one time as I had break points here the code did not error. |
I did a little Googling/Binging on the topic and it seems "normal" for WPF to set dependency properties to null in certain circumstances. I would therefore call the current implementation which I have pictured below as a bug. If you don't want to call it a bug then please add it to the feature request list. In the meantime, I tried playing with my binding's mode and update triggers but had no luck. What appears to work is to set WebView2's datacontext to null when my user control's data context is being set to null.
|
Ya sounds like if WPF itself is setting the property to null then we need to handle that and at least make sure we don't throw an error that you can't catch. I've opened this as a bug on our backlog. Thanks for digging into this more, and I'm glad you found a workaround for now! |
Yes. The cookies are stored in the user data folder, so if your webviews use the same user data folder as specified in the environment, they will be shared. They will also use the same browser processes (and GPU, network, etc.) but have separate renderer processes. More info: |
Is this issue still open and in you backlog to be fixed? |
Hey @darbid - yes this item is still on our backlog. |
I am having the same issue here. Any luck fixing this? @darbid above workaround worked for me. |
WPF tab controls tear down to save memory when not being viewed, I am not sure if this is ever going to work for webview2. In any case I am not waiting for it. I am using MahApps and their MetroTabControl with the property KeepVisualTreeInMemoryWhenChangingTab set to true. This keeps them all alive. The code that is actually doing this is ControlzEx make sure to get the most recent version as there was a bug in their code for tabs. |
I am having the same issue here |
@efsfssf and @jpgarza93 there are a number of people here who are not raising this as an issue and are using WPF tab controls. see for example #1412 which has a few people on it including @RickStrahl and @liwuqingxin @Symbai they all do not seem to have this issue and yet appear to be using a TabControl. For me the fix is not to let the TabControl tear down the non-showing tab which you can do yourself or just use ControlzEx. I am however, wondering if it is because of the method used to add tabs. I am using a dataTemplate however these other guys might be simply adding the WEBView2 directly to the DataSource of the Tabcontrol. So how are you adding your new window or second Tab? As a side note @RickStrahl and @liwuqingxin @Symbai could it be that the flickering is made worse because of how the TabControl deals with tabs that are not showing? |
Not sure why you might be insinuating my question as a complaint. Nevertheless, your suggestions are good, I am using one of them. But this issue still needs to be resolved. |
sorry sorry, not at all. My main goal was actually to see if this only happened to us here because of the method of adding to a tabcontrol. How do you add your Webview2? |
I see :) Here is how I use it: note: the browser UserControl gets inserted by a change in DataTemplate that changes on tab selection. Therefore, I had to add the UserControl_DataContextChanged event to avoid the issue. BrowserView.xaml <UserControl
x:Class="BrowserView"
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf">
<Grid>
<wv2:WebView2
x:Name="Browser"
NavigationCompleted="Browser_NavigationCompleted"
NavigationStarting="Browser_NavigationStarting"
Source="{Binding CurrentUrl, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
Visibility="{Binding IsWebView2Installed, Converter={StaticResource BooleanToVisibilityConverter}}" />
</Grid>
</UserControl> BrowserView.xaml.cs public partial class BrowserView : UserControl
{
// FIELDS
private bool _isNavigating = false;
public bool IsWebView2Installed => WebView2Install.GetInfo().InstallType != InstallType.NotInstalled;
// CONSTRUCTOR
public BrowserView()
{
InitializeComponent();
if (IsWebView2Installed)
InitializeWebView2Async();
if (Application.Current != null && Application.Current.MainWindow != null)
Application.Current.MainWindow.Closing += MainWindowOnClosing;
}
private async void InitializeWebView2Async()
{
await Browser.EnsureCoreWebView2Async(null);
/// Attach Events
Browser.NavigationStarting += EnsureHttps;
}
// METHODS
private void Hyperlink_OnClick(object sender, RoutedEventArgs e)
{
Process.Start(new ProcessStartInfo()
{
FileName = "https://go.microsoft.com/fwlink/p/?LinkId=2124703",
UseShellExecute = true
});
}
/// Go Back
void BackCmdCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = Browser != null && Browser.CanGoBack;
}
void BackCmdExecuted(object target, ExecutedRoutedEventArgs e)
{
Browser?.GoBack();
}
/// Go Forwards
void ForwardCmdCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = Browser != null && Browser.CanGoForward;
}
void ForwardCmdExecuted(object target, ExecutedRoutedEventArgs e)
{
Browser?.GoForward();
}
/// Refresh
void RefreshCmdCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = Browser != null && Browser?.CoreWebView2 != null && !_isNavigating;
}
void RefreshCmdExecuted(object target, ExecutedRoutedEventArgs e)
{
Browser?.Reload();
}
/// Go To
void GoToPageCmdCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = Browser != null && !_isNavigating;
}
async void GoToPageCmdExecuted(object target, ExecutedRoutedEventArgs e)
{
// Setting webView.Source will not trigger a navigation if the Source is the same
// as the previous Source. CoreWebView.Navigate() will always trigger a navigation.
await Browser?.EnsureCoreWebView2Async();
Browser?.CoreWebView2?.Navigate(AddressBar.Text);
}
void Browser_NavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs e)
{
_isNavigating = true;
RequeryCommands();
}
void Browser_NavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e)
{
_isNavigating = false;
RequeryCommands();
}
/// Suspend
void CoreWebView2RequiringCmdsCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = Browser != null && Browser?.CoreWebView2 != null;
}
/// Security
private void EnsureHttps(object sender, CoreWebView2NavigationStartingEventArgs args)
{
var uri = args.Uri;
if (uri.StartsWith("https://")) return;
Browser.CoreWebView2.ExecuteScriptAsync($"alert('{uri} is not safe, try an https link')");
args.Cancel = true;
}
private void RequeryCommands()
{
// Seems like there should be a way to bind CanExecute directly to a bool property
// so that the binding can take care keeping CanExecute up-to-date when the property's
// value changes, but apparently there isn't. Instead we listen for the WebView events
// which signal that one of the underlying bool properties might have changed and
// bluntly tell all commands to re-check their CanExecute status.
//
// Another way to trigger this re-check would be to create our own bool dependency
// properties on this class, bind them to the underlying properties, and implement a
// PropertyChangedCallback on them. That arguably more directly binds the status of
// the commands to the WebView's state, but at the cost of having an extraneous
// dependency property sitting around for each underlying property, which doesn't seem
// worth it, especially given that the WebView API explicitly documents which events
// signal the property value changes.
CommandManager.InvalidateRequerySuggested();
}
// DISPOSE
private void MainWindowOnClosing(object sender, CancelEventArgs e)
{
Browser.Dispose();
}
private void UserControl_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue == null)
{
this.Browser.DataContext = null;
}
}
} BrowserViewModel.cs public class BrowserViewModel : ViewModelBase
{
// FIELDS
private Uri _currentUrl = new("YourStringUrlHere", UriKind.Absolute);
// GETS & SETS
public Uri CurrentUrl
{
get => _currentUrl;
set => SetProperty(ref _currentUrl, value);
}
public bool IsWebView2Installed => WebView2Install.GetInfo().InstallType != InstallType.NotInstalled;
// CONSTRUCTOR
public BrowserViewModel()
{
}
} |
Just stumbled upon this too. I have a ContentControl which swiches content between two user controls, one containing a WebView2. Once I switch to the control not containing a WebView2 the "source property can not be null" exception is thrown. IMHO WebView2 should just show a blank page if Source is null. |
I also am suffering from this problem... it is a showstopper for the production app I'm developing. Setting the fallback value fixes the error when destroying the control, but now I get a message that the control is being initialized twice. As I want to put the Evergreen browser files in a specific location, I must initialize the control in the code-behind. Stuck once again. |
Hmmm... interesting. I don't see any issue with Tab deactivation behavior other than the annoying flashing issues I've worked around with Dispatcher timing delays ( I have a post on my blog on this). Could it be you're misinterpreting the initialization behavior? In my experience the hardest problem to overcome is that the WebView does not initialize until the parent control becomes visible. Meaning none of the code after It's ugly and causes all sorts of pain if you're using multiple controls. I've worked around all of this in Markdown Monster, but it's taking me probably a good 20-30 hours (over time) to get around all of this. Painful. Using pure MVVM will make this even harder because you lose control over exactly when some of the assignments and state changes happen. My workaround for that part of it is to use a WebViewHandler wrapper that wraps everything related to Web View access, initialization, navigation etc. so that I have an intermediary to intercept and take preventative action. I have a Westwind.WebView library and NuGet package I use for that now, but it's not well documented as it's primarily internal for my own use. There's a repo though and you can take a look at what is addressed. |
Rick, Thank you for chiming in. It doesn't look like I've successfully communicated my situation. I'm fully able to load, initialize, and use the control. I can navigate, execute JS in the page, and receive JS messages from the page. The issue I'm seeing is not when navigating to another tab, but rather when I close the view (in my MVVM framework). Prior to control destruction, the Source DP is set to null (out of my control), and an error is thrown. If it was just an error, I could trap/ignore it... sadly, it appears that the control is not properly destroyed, and garbage is left onscreen. I stated that the control is hosted in a "tab"... but it is more complicated than that. I probably should have said "container" instead of "tab". I believe this detail to be irrelevant anyway, as the error is coming from the WV2 control when the hosting container destroys it. The workaround to set the fallback value in XAML to "about:blank" addresses the issue with destruction... however, a different issue arises. By setting the fallback value, the control is initialized before the code-behind of the hosting control is executed. When I async-initialize the control, I'm informed that the control's environment has already been set. This is undesirable as when I set the environment, I set an out-of-the-way folder in which to store the Evergreen cache files. Using the fallback, these files are stored in my EXE folder... which I find inelegant. I'm hoping to find a way to set the fallback value in the code-behind, but I'm skeptical. I'll study your wrapper class implementation... perhaps the magic trick(s) I need can be found there. Thanks, Mark |
I was able to find a workaround for my issue. In my user control I hook the DataContextChanged event. In the handler method, if the data context has been set to null, I set the WebView2 page source to "about:blank". |
Hi, I'm facing an related issue too. In my scenario (.net8/Wpf) there is a collection of ViewModels whose presentations are automatically generated via binding to a Devexpress DockLayoutManager. The layout manager generates a dockable tab containing a WebView2 for each view model whose source is bound to a property of the corresponding viewodel. As soon as one of the tabs is closed the infamous Exception
is raised, which originates frome here: 500 internal static void SourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
501 {
502 IWebView2Private control = (IWebView2Private)d;
503 if (!control.webview2Base.IsPropertyChangingFromCore(SourceProperty))
504 {
505 Uri uri = (Uri)e.OldValue;
506 Uri uri2 = (Uri)e.NewValue;
507 if (uri2 == null)
508 {
509 throw new NotImplementedException("The Source property cannot be set to null.");
510 }
511 if (control.webview2Base.CoreWebView2 != null && (uri == null || uri.AbsoluteUri != uri2.AbsoluteUri))
512 {
513 control.webview2Base.CoreWebView2.Navigate(uri2.AbsoluteUri);
514 }
515 control.webview2Base._implicitInitGate.RunWhenOpen(delegate
516 {
517 control.webview2Base.EnsureCoreWebView2Async();
518 });
519 }
520 } Of course I tried the mentioned workarounds and in my frustration I even tried using a Converter which converts any provided null-value to an about:blank Uri-instance. However in my case the issue is not the databinding, apparently the culprit is the declaration of the Source-property itself: //
// Summary:
// The WPF System.Windows.DependencyProperty which backs the Microsoft.Web.WebView2.Wpf.WebView2.Source
// property.
public static readonly DependencyProperty SourceProperty = WebView2Base.SourceProperty.AddOwner(typeof(WebView2)); This declaration refers to the attached property defined in WebView2Base: public static readonly DependencyProperty SourceProperty = DependencyProperty.RegisterAttached("Source", typeof(Uri), typeof(WebView2Base), new PropertyMetadata(null, SourcePropertyChanged, null), SourcePropertyValid); The propertymetadata within this definition new PropertyMetadata(null, SourcePropertyChanged, null) defines the property's default value (first argument) as null, Looking at the top of the corresponding callstack one can see what happens:
In stack frame 6 the ClearTemplateChain method calls InvalidateProperty(dp) which resolves to InvalidateProperty(dp, preserveCurrentValue:false); thus yielding stackframe 5 in which the property is set to its default value (null) and triggering the exception in stackframe 1. From my limited point of view I'd say, that a property which does not allow null as a value, it does not make much sense to define its default value to be null. Any thoughts? |
For the time being I helped myself by using an attached property as a drop-in replacement, which assures that only non-null values are passed on to WebView2.Source: using System;
using System.Windows;
using Microsoft.Web.WebView2.Wpf;
namespace FeelFreeToPickOne;
internal static class CoercedSource {
public static readonly DependencyProperty SourceProperty = DependencyProperty.RegisterAttached(
"Source",
typeof(Uri),
typeof(CoercedSource),
new PropertyMetadata(AboutBlank, PropertyChanged, CoerceSource)
);
public static Uri GetSource(DependencyObject obj)
=> (Uri)obj.GetValue(SourceProperty);
public static void SetSource(DependencyObject obj, Uri value)
=> obj.SetValue(SourceProperty, value);
private static object CoerceSource(DependencyObject d, object baseValue)
=> baseValue switch {
null => GetSource(d),
_ => baseValue
};
private static void PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
if (d is WebView2 webView2) {
webView2.Source = (Uri)e.NewValue;
}
}
private static readonly Uri AboutBlank = new ("about:blank");
} By replacing the existing binding for which the exception is risen <wv2:WebView2 Source="{Binding Path=HtmlSourceUri, Mode=OneWay}" /> with the following <wv2:WebView2 local:CoercedSource.Source="{Binding Path=HtmlSourceUri, Mode=OneWay}"/> the issue is gone. |
I am working on a WPF app with dynamically added TabItems. Further I am attempting to follow MVVM concepts.
I have set up an example repo Example WPF TabControl
In the example repo the TabControl's ItemSource is set to an ObservableCollection (name: TabCollection) of a UserControl's (name: BrowserTabItemUC) view model (name: BrowserTabItemViewModel). This UserControl holds the WebView2 control.
To set up the TabItem I have a ContentTemplate as follows;
When a new tab is requested I add a new instance of the viewmodel to the TabCollection. This is where I have issues with creating the new WebView2. I am currently able to add a new Source to WebView2 from the ViewModel as Source is a Dependency property of the Webview2, however, how does one add an existing environment? This question is also my main issue with trying to implement MVVM concepts. CoreWebView2 is not a Dependency Property and thus all of these properties, methods and events are not available to a view model.
Generally I am wondering how the WebView2 should work in such a case, especially for WPF TabControl as the whole WebView2 re-initializes when manually changing tabs.
Possible bug
In my example simply close a second tab. When the collection attempts to remove the item I get an error with respect to the Source property being null. See the pic.
I cannot reproduce a second error i sometimes get. When manually changing tabs sometimes the Source become null as well and WebView2 does not like this null value as well. As i said i cannot reproduce this all the time so am assuming it is my code.
AB#32376214
The text was updated successfully, but these errors were encountered: