Sharing XAMLSo far we have discussed some techniques on how to share C# code between our WPF and Universal Apps. However, we all know that C# is only half the story. A large portion of the application is written in XAML code and it is just as important to be able to share it between the projects. Sharing simple stuff is somewhat straightforward and some general approaches are discussed here. However, this is not so interesting. What I would like to discuss today is a tedious incompatibility between XAML in UniversalApps/WinRT and WPF. It is pretty much the most substantial roadblock when trying to share XAML code between these platforms.
The clr-namespace/using issueIf you are not familiar with the issue yet, here is a short explanation. In XAML we declare namespace prefixes as a way to incorporate code from different namespaces. However, it is done differently in different XAML implementations: WPF/Silverlight/Windows Phone 8 way: xmlns:controls="clr-namespace:MyApp.Controls" WinRT/Universal App way: xmlns:controls="using:MyApp.Controls" Using one platform's syntax will fail to compile on the other and vice versa. This is it. The small difference in "clr-namespace vs using" is the little stone that derails the cart and causes anguish and suffering to XAML developers across the world. It looks innocent enough, but this is a inconvenient roadblock when trying to reuse XAML files across different projects. However, before looking at how to solve this, I would like to make a small stop and talk about a possible solution that is painfully close but unfortunately doesn't work.
Custom XML Namespaces - The solution that wasn'tWPF and Silverlight allow developers to create custom XML namespaces and prefixes for XAML. What this means is that you can turn this: xmlns:controls="clr-namespace:MyApp.Controls" into this: xmlns:controls="http://schemas.newventuresoftware.com/controls" This is discussed in greater detail in these two articles. Why am I mentioning this? Well, if we can define all namespaces in this manner we will get rid of the clr-namespace/using incompatibility. Indeed this is the reason "simple" XAML sharing works out of the box - if we stick to the "built-in" namespaces such as: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" and xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" things just work well. So why can't we use this approach? Well, as you have probably seen from the articles these custom definitions works through the use of XmlnsDefinitionAttribute and XmlnsPrefixAttribute. To my incredible disappointment these two are not available in the Universal Apps. Please, Microsoft, ship them! Pretty please! If this inconsistency did not exist it would have solved today's problem in a very elegant way.
Adding a custom template control to our demo appLet's go back to our "MyApp" application and try to create a custom templated control. We will name it MyTempaltedControl (yes, so original) and place it in the Shared Project like this:
First, let's quickly look at the C# implementation:
You will notice the different approach to setting the control DefaultStyleKey. However, thanks to C# conditional compilation directives we are able to resolve those differences within the same source file. Now, let's take a look at the shared XAML code:
We will go with the simplest template possible - in this case I've chosen the one generated by Visual Studio. If we build the project the universal apps will compile just fine, but the WPF project will fail (as we expected) since the "local" namespace prefix is defined with the "using" syntax that is incompatible with WPF (which expects clr-namespace). In order to fix this we are going to have to do some XAML Preprocessing.
XAML Preprocessing - The hands on approachThe idea is simple. When compiling the WPF project we should inject some custom code of ours into the build process which replaces the "using:" part of the prefix declarations with "clr-namespace:" and then passes it to the XAML compiler in it's modified state. Let's split the task in two parts - replacing the markup (one) and modifying the build process (tw0).
Building the XamlPreprocessor.exeFor the purposes of this article we are going to build our XamlPreprocessor as a console application. It is extremely simple, here is the code:
What the app does is very straightforward - it should be executed with two parameters - the path to an input file and the path to an output file. It reads the input file, applies the regular expression which matches the "using:" syntax and replaces it with "clr-namespace:" syntax and writes the modified contents to the output file. I am adding this console application to the main MyApp solution and I have added a post-build event "xcopy "$(TargetPath)" "$(SolutionDir)\MyApp\MyApp.Desktop\" /Y" to copy the executable to the WPF project directory to be ready to be integrated with MSBuild. Finally, you can set the WPF project to depend on the XamlPreprocessor so that it is guaranteed to be always built first.
Integration with MSBuild - Writing our custom targetIn order to run our XamlPreprocessor for each XAML file within the WPF project we need to modify the .csproj file and add a new build target. First, here is the code to the target:
As you can see from the code, we are hooking our target before the MarkupCompilePass1 which is exactly the moment before the XAML gets compiled. We will save the code above to a PreprocessXaml.targets file and include it in the MyApp.Desktop.csproj file in order for it to be called.
ConclusionA couple of closing points. The preprocessor discussed here is very simple and only has a single function. If you are looking for something more feature rich which translates to #if/#else C# statements but for XAML you may want to look at XCC. Second, the approach I have taken with an executable console app is fine, but it may be worth considering implementing your own custom MSBuild Task if you are going to be doing more serious stuff. Lastly, you can find the updated full source code to the
MyApp demo here: MyApp-master Happy coding!