I encountered a XAML puzzle today. Luckily, it was easy to solve although it did take some time to find out.
Suppose that you want to keep part of your WPF UI in a file external to your main application. Normally, all of your XAML is embedded as a Page resource in your application. This translates to a compiled XAML, also known as BAML. Allowing the outside world to specify a piece of XAML that you will show in your app can be useful in a number of scenarios. It is not something you will do every day but there are applications that could hardly do without this.
Now let’s suppose you create a brand new UserControl in your Visual Studio project. You will get a UserControl1.xaml and a UserControl1.xaml.cs. We can’t (or don’t want to) load code so just remove the .CS file. Now, we add something specific. We want to reference say some attached property from our own project. Let’s say the namespace is Email and the property is Caption on class Details.
<UserControl x:Class="Email.UserControl1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Email="clr-namespace:Email" Email:Details.Caption="Test"> <Grid> <TextBlock Text="Hey!" /> </Grid> </UserControl>
How can we load this XAML file in our application and make it work as if it was compiled as part of the project? XamlReader comes to mind. Let’s try.
var userControl = XamlReader.Load(fileStream) as UserControl; var caption = Email.Details.GetCaption(userControl);
Ok, this works but if we check the value of attached property we will discover it has a default value. Now why is that? Maybe we are using a wrong approach. If we try to load this with Application.LoadComponent() it will succeed beautifully.
var userControl = Application.LoadComponent(new Uri("/Email;component/usercontrol.xaml", UriKind.Relative)) as UserControl;
That worked with a local resource but what if we want to use LoadComponent() to load the file external to the application from an arbitrary URL? No, that won’t work. LoadComponent() will throw an exception complaining about the absolute URL. Basically, you can’t use LoadComponent() on anything but a local URL.
So what are our options? Huh, reflection perhaps? Doing what LoadComponent() does but skipping the relative URL check? No, that won’t work because relative URLs are there for a reason. LoadComponent() really loads from BAML not XAML.
After hours of pondering, searching and trial and error I finally came up with a surprisingly simple solution. We go back to the trusted XamlReader. What we need to do is just slightly change the xmlns:Email declaration by adding the assembly=Email like this:
<UserControl x:Class="Email.UserControl1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Email="clr-namespace:Email;assembly=Email" Email:Details.Caption="Test"> <Grid> <TextBlock Text="Hey!" /> </Grid> </UserControl>
Voila! The XAML is finally loaded properly with our attached property accurately set.