loading ResourceDictionary from baml using Baml2006Reader - wpf

How do can I read through a baml stream that contains a ResourceDictionaory using the Baml2006Reader and without acually instantiating the the ResourceDictionary?
I can ready through regular baml that just contains a UserControl just fine and I can examine the XAML tree using Baml2006Reader.NodeType etc.
But once the reader hits a ResourceDictionary, Baml2006Reader.Member.Name is "DeferrableContent" and Baml2006Reader.Value contains a MemoryStream that can not be parsed by another instance of Baml2006Reader. I can't event instantiate the reader:
System.IO.EndOfStreamException occurred HResult=-2147024858
Message=Unable to read beyond the end of the stream. Source=mscorlib
StackTrace:
at System.IO.MemoryStream.InternalReadInt32()
at System.Windows.Baml2006.Baml2006Reader.Process_Header()
at WpfApplication10.AssemblyExtensions.Read(Stream stream, List`1 result) in d:\Documents\Visual Studio
2012\Projects\WpfApplication10\WpfApplication10\AssemblyExtensions.cs:line
84 InnerException:

It seems that whenever the Baml2006Reader encounters an element where Baml2006Reader.Member.Name is "DeferrableContent" it is followed by another node where BamlReader.Value is a MemoryStream. It seems that this stream only contains a baml fragment and does not have a header (that's why System.Windows.Baml2006.Baml2006Reader.Process_Header() fails.)
So we need to tell the baml reader to read a baml fragment. This can be done be giving the reader an instance of System.Windows.Baml2006.Baml2006ReaderSettings where the IsBamlFragment property istrue.
Unfortunately both the Baml2006ReaderSettings class and the appropriate constructor of Baml2006Reader are internal. So we need to resort to reflection:
private static string PresentationFrameworkAssemblyName = "PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35";
private static Baml2006Reader CreateBamlFragmentReader(MemoryStream substream, XamlSchemaContext schemaContext)
{
var bamlSettingsType =
Type.GetType(
"System.Windows.Baml2006.Baml2006ReaderSettings, " + PresentationFrameworkAssemblyName);
var settingsCtor =
bamlSettingsType.GetConstructor(Type.EmptyTypes);
var bamlSettings = settingsCtor.Invoke(null);
var isBamlFragmentProp = bamlSettingsType.GetProperty("IsBamlFragment",
BindingFlags.NonPublic |
BindingFlags.Instance);
isBamlFragmentProp.SetValue(bamlSettings, true, null);
var ctor = typeof (Baml2006Reader).GetConstructor(
BindingFlags.Instance | BindingFlags.NonPublic,
null,
new[]
{
typeof (Stream),
Type.GetType(
"System.Windows.Baml2006.Baml2006SchemaContext, " + PresentationFrameworkAssemblyName),
bamlSettingsType
},
null);
return (Baml2006Reader)ctor.Invoke(new[] { substream, schemaContext, bamlSettings });
}
usage:
var substream = reader.Value as MemoryStream;
if (substream != null)
{
using (var subReader = CreateBamlFragmentReader(substream, reader.SchemaContext))
{
// continue reading with subReader
}
}
I know this is rather fragile code and very hackish, but what the heck - it works (for me, currently)!

Related

DocumentViewer to RichTextBox Binding Error

I have an application with RichTextBox and DocumentViewer (placed in a TabControl), and I want to make something like "hot preview". I've binded DocumentViewer.Document property to RichTextBox.Document
Binding:
<DocumentViewer Document="{Binding Document, Converter={StaticResource FlowDocumentToPaginatorConverter}, ElementName=mainRTB, Mode=OneWay}" />
And this is Converter code:
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
FlowDocument d = value as FlowDocument;
DocumentPaginator pagin = ((IDocumentPaginatorSource)d).DocumentPaginator;
FixedDocumentSequence result = null;
Size s = new Size(793.700787402, 1122.519685039);
pagin.PageSize = s;
using (MemoryStream ms = new MemoryStream())
{
TextRange tr = new TextRange(d.ContentStart, d.ContentEnd);
tr.Save(ms, DataFormats.XamlPackage);
Package p = Package.Open(ms, FileMode.Create, FileAccess.ReadWrite);
Uri uri = new Uri(#"memorystream://doc.xps");
PackageStore.AddPackage(uri, p);
XpsDocument xpsDoc = new XpsDocument(p);
xpsDoc.Uri = uri;
XpsDocument.CreateXpsDocumentWriter(xpsDoc).Write(pagin);
result = xpsDoc.GetFixedDocumentSequence();
}
return result;
}
When I start this application everything is ok until I switch to tab with DocumentViewer. Application crushes and I get such Exception:
Cannot perform a read operation in write-only mode.
What I am doing wrong? Is it possible to make this binding?
The error message is indeed confusing and reason not immediately obvious. Basically you are closing the MemoryStream that holds XpsDocument too early and when the DocumentViewer attempts to read the document it cannot as it is write-only mode (because the stream was closed).
The solution is to not immediately close the MemoryStream until after you have finished viewing the document. To achieve this I wrote an XpsDocumentConverter that returns XpsReference.
Also, as you never been able to convert and display a single XpsDocument you won't have yet encountered the next issue of having multiple packages in the PackageStore with the same Uri. I have taken care of this in my implementation below.
public static XpsDocumentReference CreateXpsDocument(FlowDocument document)
{
// Do not close the memory stream as it still being used, it will be closed
// later when the XpsDocumentReference is Disposed.
MemoryStream ms = new MemoryStream();
// We store the package in the PackageStore
Uri uri = new Uri(String.Format("pack://temp_{0}.xps/", Guid.NewGuid().ToString("N")));
Package pkg = Package.Open(ms, FileMode.Create, FileAccess.ReadWrite);
PackageStore.AddPackage(uri, pkg);
XpsDocument xpsDocument = new XpsDocument(pkg, CompressionOption.Normal, uri.AbsoluteUri);
// Need to force render the FlowDocument before pagination.
// HACK: This is done by *briefly* showing the document.
DocumentHelper.ForceRenderFlowDocument(document);
XpsSerializationManager rsm = new XpsSerializationManager(new XpsPackagingPolicy(xpsDocument), false);
DocumentPaginator paginator = new FixedDocumentPaginator(document, A4PageDefinition.Default);
rsm.SaveAsXaml(paginator);
return new XpsDocumentReference(ms, xpsDocument);
}
public class XpsDocumentReference : IDisposable
{
private MemoryStream MemoryStream;
public XpsDocument XpsDocument { get; private set; }
public FixedDocument FixedDocument { get; private set; }
public XpsDocumentReference(MemoryStream ms, XpsDocument xpsDocument)
{
MemoryStream = ms;
XpsDocument = xpsDocument;
DocumentReference reference = xpsDocument.GetFixedDocumentSequence().References.FirstOrDefault();
if (reference != null)
FixedDocument = reference.GetDocument(false);
}
public void Dispose()
{
Package pkg = PackageStore.GetPackage(XpsDocument.Uri);
if (pkg != null)
{
pkg.Close();
PackageStore.RemovePackage(XpsDocument.Uri);
}
if (MemoryStream != null)
{
MemoryStream.Dispose();
MemoryStream = null;
}
}
}
XpsReference implements IDisposable so remember to call Dispose() on it.
Also, once you resolve the above error the next problem you are likely to encounter will be content not rendering as you would expect. This is caused by the fact you need to clone FlowDocument and it has not undergone a full measure and arrange layout pass. Read
Printing BlockUIContainer to XpsDocument/FixedDocument on how to solve this.

Silverlight import App.xaml from dynamically xap loaded

Is there any way to access app.xaml resources of an imported XAP file (from host silverlight app) ???
Or better, import guest app.xaml into Host app.xaml
Problem is that in the imported Silverlight application, I lost all app.xaml resources, and I just see host resources... I would like to merge them...
It's possible?
I load XAP in this way
private void wc_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
var manifestStream = Application.GetResourceStream(
new StreamResourceInfo(e.Result, null),
new Uri("AppManifest.xaml", UriKind.Relative));
string appManifest = new StreamReader(manifestStream.Stream).ReadToEnd();
string assemblyName =m_rootAssembly + ".dll";
XmlReader reader = XmlReader.Create(new StringReader(appManifest));
Assembly asm = null;
while (reader.Read())
{
if (reader.IsStartElement("AssemblyPart"))
{
reader.MoveToAttribute("Source");
reader.ReadAttributeValue();
if (reader.Value == assemblyName)
{
var assemblyStream = new StreamResourceInfo(e.Result, "application/binary");
var si = Application.GetResourceStream(assemblyStream, new Uri(reader.Value, UriKind.Relative));
AssemblyPart p = new AssemblyPart();
asm = p.Load(si.Stream);
break;
}
}
}
if (asm == null)
throw new InvalidOperationException("Could not find specified assembly.");
var o = asm.CreateInstance(m_typeName);
if (o == null)
throw new InvalidOperationException("Could not create instance of requested type.");
RaiseXapLoadedEvent(o);
}
if you know the uri of the resource file, like:
"/COMPONENTNAME;component/resourcepath.xaml",
you can simply add this resource dictionary to your application by writing codes like:
Application.Current.Resources.MergedDictionaries.Add("*resourceuri*");
the "Application.Current" always points to the runtime application instance (host application), no matter where it is used.

How can I deserialize with TypeNameHandling.Objects in Json.NET Silverlight?

I get an exception when trying to deserialize in Silverlight. Test1 fails, while Test2 succeeds. I've also tried TypeNameAssemblyFormat to both Simple and Full, but get same results. Test2 can resolve the assembly, why can't Json.NET?
Update: Forgot to mention the type I'm trying to deserialize is defined in a different assembly from the silverlight assembly where the deserialization occurs.
Both tests work in a non-silverlight .NET application.
How can I deserialize a json string that has typenames?
private void Test1()
{
JsonSerializerSettings settings = new JsonSerializerSettings();
settings.TypeNameHandling = TypeNameHandling.Objects;
string json1 = "{\"$type\":\"AmberGIS.NetworkTrace.DTO.NTPoint, NetworkTrace.DTO.Assembly\",\"X\":0.0,\"Y\":0.0,\"SpatialReference\":null}";
try
{
var n1 = JsonConvert.DeserializeObject<NTPoint>(json1, settings);
//Error resolving type specified in JSON 'AmberGIS.NetworkTrace.DTO.NTPoint, NetworkTrace.DTO.Assembly'.
//Could not load file or assembly 'NetworkTrace.DTO.Assembly, Culture=neutral, PublicKeyToken=null' or one of its dependencies.
//The requested assembly version conflicts with what is already bound in the app domain or specified in the manifest.
//(Exception from HRESULT: 0x80131053)
}
catch (Exception ex)
{
while (ex != null)
{
Debug.WriteLine(ex.Message);
ex = ex.InnerException;
}
}
}
This Test2 succeeds:
private void Test2()
{
var pnt1 = new AmberGIS.NetworkTrace.DTO.NTPoint();
Debug.WriteLine(pnt1.GetType().AssemblyQualifiedName);
// "AmberGIS.NetworkTrace.DTO.NTPoint, NetworkTrace.DTO.Assembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
string fullName = "AmberGIS.NetworkTrace.DTO.NTPoint, NetworkTrace.DTO.Assembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null";
var t = Type.GetType(fullName);
var pnt2 = Activator.CreateInstance(t) as NTPoint;
}
Try adding settings to JsonConvert.DeserializeObject<T>(json, Settings),
where Settings is:
new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Objects,
TypeNameAssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Full
}
I resolved my issue by downloading source for Json.NET 4.0r2, and adding 2 lines of hack code to DefaultSerializationBinder.cs, as shown below. This probably won't work for strong named assemblies. Silverlight lacks a method to scan the appdomain for loaded assemblies, see here.
#if !SILVERLIGHT && !PocketPC
// look, I don't like using obsolete methods as much as you do but this is the only way
// Assembly.Load won't check the GAC for a partial name
#pragma warning disable 618,612
assembly = Assembly.LoadWithPartialName(assemblyName);
#pragma warning restore 618,612
#else
// **next 2 lines are my hack** ...
string fullName = String.Format("{0}, {1}, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",typeName,assemblyName);
return Type.GetType(fullName);
assembly = Assembly.Load(assemblyName);
#endif
I am posting my solution here that does not require modifying Json.NET:
The problem is that the following line is not sufficient for Silverlight:
string json1 = "{\"$type\":\"AmberGIS.NetworkTrace.DTO.NTPoint, NetworkTrace.DTO.Assembly\" ... }";
It needs:
string json1 = "{\"$type\":\"AmberGIS.NetworkTrace.DTO.NTPoint, NetworkTrace.DTO.Assembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null \", ...}";
So my way of including that in the JSON (in my case the JSON could not be changed since it was coming from a server and wasn't generated by JSON.net) is to manually modify the JSON by iterating over all (nested) objects and inserting the assembly info:
string json = <some json you want fixed>
Type type = <the target type you want>
JObject jsonObject = JObject.parse (json);
jsonObject["$type"] = type.FullName + ", " + type.Assembly.FullName;
json = jsonObject.ToString(Formatting.None, null);
Then you can deserialize as usual using
JsonSerializerSettings settings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All };
var n1 = JsonConvert.DeserializeObject<NTPoint>(json, settings);

How to get the application's ProductName in Silverlight

Is it possible to programmatically get the ProductName of a Silverlight application? I'm looking for the Silverlight equivalent of this WinForms/WPF instruction:
string productName = System.Windows.Forms.Application.ProductName;
Thank you
You can use the following method, but only if you can guarantee that you call it from the "entry" assembly.
public static void GetProductAndVersionEasy(out string productName, out Version productVersion)
{
var callingAssembly = Assembly.GetCallingAssembly();
// Get the product name from the AssemblyProductAttribute.
// Usually defined in AssemblyInfo.cs as: [assembly: AssemblyProduct("Hello World Product")]
var assemblyProductAttribute = ((AssemblyProductAttribute[])callingAssembly.GetCustomAttributes(typeof(AssemblyProductAttribute),false)).Single();
productName = assemblyProductAttribute.Product;
// Get the product version from the assembly by using its AssemblyName.
productVersion = new AssemblyName(callingAssembly.FullName).Version;
}
(You can replace GetCallingAssembly with GetExecutingAssembly if the method is in your entry assembly).
I also figured out how to hack this information from the .xap file. I load the main assembly's bytes into memory, then read the product and version info from the last few bytes. I had to write this because I needed a method I could reuse in a base library which can be called from anywhere (ie. not the executing or calling assembly).
public static void GetProductAndVersionHack(
out string productName,
out Version productVersion)
{
// Get the name of the entry assembly
var deployment = System.Windows.Deployment.Current;
var entryPointAssembly = deployment.EntryPointAssembly + ".dll";
// Get the assembly stream from the xap file
StreamResourceInfo streamResourceInfo = Application.GetResourceStream(
new Uri(
entryPointAssembly,
UriKind.Relative));
Stream stream = streamResourceInfo.Stream;
// The VERSION_INFO struct as at the end of the file. Just read the last 1000 bytes or so from the stream
// (Keep in mind that there are a lot of zeroes padded at the end of the stream. You should probably
// search for the real stream.Length after trimming them off)
stream.Position = stream.Length - 1000;
StreamReader streamReader = new StreamReader(stream, Encoding.Unicode);
string text = streamReader.ReadToEnd();
// Split the string on the NULL character
string[] strings = text.Split(
new[] { '\0' },
System.StringSplitOptions.RemoveEmptyEntries);
// Get the Product Name (starts with unicode character \u0001)
int ixProductName = strings.FindIndexOf(line => line.EndsWith("\u0001ProductName"));
productName = ixProductName >= 0 ? strings[ixProductName + 1] : null;
// Get the Product Version
int ixProductVersion = strings.FindIndexOf(line => line.EndsWith("\u0001ProductVersion"));
productVersion = ixProductVersion >= 0 ? Version.Parse(strings[ixProductVersion + 1]) : null;
}
public static int FindIndexOf<T>(
this IEnumerable<T> source,
Func<T, bool> match)
{
int i = -1;
foreach (var item in source)
{
++i;
if (match(item)) return i;
}
return -1;
}
Not sure what device your deving for, but for WP7 silverlight, In the Properties folder contained a file called WMAppManifest.xml, and under Deployment>App their is a field called Title, you can pull the XML out of there?
I hope that helps.
The AssemblyName class provides parsing of the Assembly FullName property.
Try this
string productName = (new System.Reflection.AssemblyName(this.GetType().Assembly.FullName)).Name;

How do I suppress script errors when using the WPF WebBrowser control?

I have a WPF application that uses the WPF WebBrowser control to display interesting web pages to our developers on a flatscreen display (like a news feed).
The trouble is that I occasionally get a HTML script error that pops up a nasty IE error message asking if I would like to "stop running scripts on this page". Is there a way to suppress this error checking?
NOTE: I have disabled script debugging in IE settings already.
Here is a solution i just made with reflection. Solves the issue :)
I run it at the Navigated event, as it seems the activeX object is not available until then.
What it does is set the .Silent property on the underlying activeX object. Which is the same as the .ScriptErrorsSuppressed property which is the Windows forms equivalent.
public void HideScriptErrors(WebBrowser wb, bool Hide) {
FieldInfo fiComWebBrowser = typeof(WebBrowser).GetField("_axIWebBrowser2", BindingFlags.Instance | BindingFlags.NonPublic);
if (fiComWebBrowser == null) return;
object objComWebBrowser = fiComWebBrowser.GetValue(wb);
if (objComWebBrowser == null) return;
objComWebBrowser.GetType().InvokeMember("Silent", BindingFlags.SetProperty, null, objComWebBrowser, new object[] { Hide });
}
A better version that can be run anytime and not after the .Navigated event:
public void HideScriptErrors(WebBrowser wb, bool hide) {
var fiComWebBrowser = typeof(WebBrowser).GetField("_axIWebBrowser2", BindingFlags.Instance | BindingFlags.NonPublic);
if (fiComWebBrowser == null) return;
var objComWebBrowser = fiComWebBrowser.GetValue(wb);
if (objComWebBrowser == null) {
wb.Loaded += (o, s) => HideScriptErrors(wb, hide); //In case we are to early
return;
}
objComWebBrowser.GetType().InvokeMember("Silent", BindingFlags.SetProperty, null, objComWebBrowser, new object[] { hide });
}
If any issues with the second sample, try swapping wb.Loaded with wb.Navigated.
Just found from another question, this is elegant and works great.
dynamic activeX = this.webBrowser1.GetType().InvokeMember("ActiveXInstance",
BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.NonPublic,
null, this.webBrowser1, new object[] { });
activeX.Silent = true;
The problem here is that the WPF WebBrowser did not implement this property as in the 2.0 control.
Your best bet is to use a WindowsFormsHost in your WPF application and use the 2.0's WebBrowser property: SuppressScriptErrors. Even then, you will need the application to be full trust in order to do this.
Not what one would call ideal, but it's pretty much the only option currently.
I've also found an interesting way to disable JavaScript errors. But you need to use at least .Net Framework 4.0 because of using elegant dynamic type.
You need to subscribe to the LoadCompleted event of the WebBrowser element:
<WebBrowser x:Name="Browser"
LoadCompleted="Browser_OnLoadCompleted" />
After that you need to write an event handler that looks like below:
void Browser_OnLoadCompleted(object sender, NavigationEventArgs e)
{
var browser = sender as WebBrowser;
if (browser == null || browser.Document == null)
return;
dynamic document = browser.Document;
if (document.readyState != "complete")
return;
dynamic script = document.createElement("script");
script.type = #"text/javascript";
script.text = #"window.onerror = function(msg,url,line){return true;}";
document.head.appendChild(script);
}
I wanted to add this as a comment to #Alkampfer answer, but I don't have enough reputation. This works for me (Windows 8.1, NET 4.5):
window.Browser.LoadCompleted.Add(fun _ ->
window.Browser.Source <- new System.Uri("javascript:window.onerror=function(msg,url,line){return true;};void(0);"))
This code sample is written in F#, but it's pretty clear what it does.
Check the below code for suppressing script errors for WPF browser control..
public MainWindow
{
InitializeComponent();
WebBrowserControlView.Navigate(new Uri("https://www.hotmail.com"));
//The below checks for script errors.
ViewerWebBrowserControlView.Navigated += ViewerWebBrowserControlView_Navigated;
}
void ViewerWebBrowserControlView_Navigated(object sender, NavigationEventArgs e)
{
BrowserHandler.SetSilent(ViewerWebBrowserControlView, true); // make it silent
}
public static class BrowserHandler
{
private const string IWebBrowserAppGUID = "0002DF05-0000-0000-C000-000000000046";
private const string IWebBrowser2GUID = "D30C1661-CDAF-11d0-8A3E-00C04FC9E26E";
public static void SetSilent(System.Windows.Controls.WebBrowser browser, bool silent)
{
if (browser == null)
MessageBox.Show("No Internet Connection");
// get an IWebBrowser2 from the document
IOleServiceProvider sp = browser.Document as IOleServiceProvider;
if (sp != null)
{
Guid IID_IWebBrowserApp = new Guid(IWebBrowserAppGUID);
Guid IID_IWebBrowser2 = new Guid(IWebBrowser2GUID);
object webBrowser;
sp.QueryService(ref IID_IWebBrowserApp, ref IID_IWebBrowser2, out webBrowser);
if (webBrowser != null)
{
webBrowser.GetType().InvokeMember("Silent", BindingFlags.Instance | BindingFlags.Public | BindingFlags.PutDispProperty, null, webBrowser, new object[] { silent });
}
}
}
}
[ComImport, Guid("6D5140C1-7436-11CE-8034-00AA006009FA"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IOleServiceProvider
{
[PreserveSig]
int QueryService([In] ref Guid guidService, [In] ref Guid riid, [MarshalAs(UnmanagedType.IDispatch)] out object ppvObject);
}
Whereas, If you are using Winforms Web browser with winforms host.. you have a property "SuppressScriptErrors" set it to true
<WindowsFormsHost Name="WinformsHost" Grid.Row="1">
<winForms:WebBrowser x:Name="WebBrowserControlView" ScriptErrorsSuppressed="True" AllowWebBrowserDrop="False"></winForms:WebBrowser>
</WindowsFormsHost>
I've this problem in the past and finally resolved it with an injection of a Javascript script that suppress error handling. Hope this could help you too.
Disable Javascript errors in WEbBrowsercontrol
you can use this trick
vb.net
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" (ByVal hwnd As Integer, ByVal wMsg As Integer, ByVal wParam As Integer, ByVal lParam As Integer) As Integer
Private Const WM_CLOSE As Short = &H10s
and call last lib :
dim hwnd
dim vreturnvalue
hwnd = FindWindow(vbNullString,"script error")
if hwnd<>0 then vreturnvalue = SendMessage(hwnd, WM_CLOSE, &O0s, &O0s)
Wolf5's answer is good but if (objComWebBrowser == null) doesn't seem to work. Instead I check the WebBrowser instance for IsLoaded:
public void HideScriptErrors(WebBrowser webBrowser)
{
if (!webBrowser.IsLoaded)
{
webBrowser.Loaded += WebBrowser_Loaded; // in case we are too early
return;
}
var objComWebBrowser = typeof(WebBrowser).GetField("_axIWebBrowser2", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(webBrowser);
if (objComWebBrowser == null)
{
return;
}
objComWebBrowser.GetType().InvokeMember("Silent", BindingFlags.SetProperty, null, objComWebBrowser, new object[] { true });
}
private void WebBrowser_Loaded(object sender, RoutedEventArgs e)
{
var webBrowser = sender as WebBrowser;
webBrowser.Loaded -= WebBrowser_Loaded;
HideScriptErrors(webBrowser);
}
It is also necessary to remove the Loaded event handler after the first time, as the control may be unloaded and loaded again several times when making it invisible by switching to a different tab. Also the if (!wb.Loaded)) fallback is still important when the WebBrowser is not visible yet on its first navigation, e.g. if it is on a different tab that is not visible on application startup.

Resources