Background
The incompatibility of XAML namespaces and available controls between Silverlight and WPF is a well-known problem. MSDN has some pretty detailed documentation on this issue. The Silverlight team has made some great efforts to improve on this in the Silverlight 3 Beta by introducing some new features that increase its compatibility with WPF. Nevertheless, there are still plenty of areas where platform inconsistencies stand in the way of having shared XAML markup and code between Silverlight and WPF. The upside to this situation is that most of these issues can be worked around using some simple techniques and a custom tool.
My product team is currently working on the user experience for both web-based and Windows-based administration consoles using Silverlight and WPF, respectively. Since we have pretty hard deadlines for when the product is to be shipped, it makes sense for us to save some time by sharing XAML and code for controls that might need to appear in both contexts. I did some research on what it would take to make this possible and came across very little information in the way of handling hard platform inconsistencies. Ben Lemmon recently posted a nice technique that uses the C++ preprocessor to block off areas of XAML only meant for either platform. One thing to note about this technique is that it results in XAML that can't be normally processed by the Visual Studio Designer or Expression Blend because it throws #ifdef blocks into otherwise normal XAML code. My goal was to go one step further to find a method that would result in valid XAML.
The technique
The technique that I created is based on use of the mc:Ignorable attribute to mark Silverlight-incompatible elements and attributes in XAML files that are meant to be shared on both platforms. This is a very useful extension that allows you to select namespaces being used in the XAML file that should be ignored by the XAML parser. When an element or attribute that is tagged with an "ignorable" namespace is reached, the XAML parser throws it away and moves on. This is a nice thing to use for WPF-specific elements in Silverlight-targeted XAML since it is technically the subset of what is available in WPF. For example, the TextBlock control exists in both Silverlight and WPF, but the TextTrimming attribute on that control only exists in WPF. Using the mc:Ignorable attribute on a namespace titled 'WPF', the resulting XAML would look like this:
<TextBlock Text="Here is a bunch of sample text" TextWrapping="NoWrap" WPF:TextTrimming="WordEllipsis"/>
If you compile this code in Silverlight or WPF the TextTrimming tag will be ignored. Knowing that we have this ability, it is possible to write a simple preprocessor that can convert this XAML from Silverlight to WPF compatibility just by removing the 'WPF' namespace from all elements or attributes marked as such.
This technique can also be used for replacing the assembly that is being used for a given namespace. The DataGrid control is a good example of this as it exists in different assemblies in Silverlight and the WPF Toolkit. Here is a more elaborate XAML snippet showing this in use:
<UserControl x:Class="PreprocessorExample.DataGridExample" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:WPF="WPF" mc:Ignorable="WPF" xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data" WPF:data="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit"> <Grid x:Name="LayoutRoot"> <data:DataGrid ItemsSource="{Binding TestList}" AutoGenerateColumns="False"> <data:DataGrid.Columns> <data:DataGridTextColumn Header="Name" Binding="{Binding Name}" /> <data:DataGridTextColumn Header="Value" Binding="{Binding Value}" /> </data:DataGrid.Columns> </data:DataGrid> </Grid></UserControl>
The root UserControl element includes the "mc" namespace which supplies the Ignorable attribute that this technique depends on. You can also see where the "WPF" namespace tag is declared (with an otherwise invalid target namespace) and then defined as ignorable. If compiled in Silverlight, this XAML would work just fine because it will pull the DataGrid pieces from the right assembly location in the SDK. To make the XAML work against the WPF Toolkit, the "data" namespace definition needs to be replaced with the one referenced by "WPF:data". My tool recognizes any WPF-tagged namespace redefinitions (or additions) and converts them into the proper xmlns declaration after removing the old declaration.
If you had a control that should only show up in WPF and required the use of a namespace declaration, you could use the same "WPF" prefix in the root element with an arbitrary namespace name. The preprocessor is aware of this and can convert it into a real namespace declaration using whatever assembly is referenced in the attribute's value. To refer to this namespace in an element that requires the use of this namespace, you would prefix the element name with the new namespace name and an underscore. Here is an example of what this would look like in practice if we wanted to add a new namespace called "special" only in WPF:
<UserControl x:Class="PreprocessorExample.WPFOnlyExample" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:WPF="WPF" mc:Ignorable="WPF" WPF:special="clr-namespace:Some.Namespace;assembly=SomeAssembly"> <Grid x:Name="LayoutRoot"> <WPF:special_WPFOnlyControl SomeProperty="true" /> </Grid></UserControl>
When this code gets run through the preprocessor, the element name would be converted from"WPF:special_WPFOnlyControl" to "special:WPFOnlyControl". This trick also works for element attributes by using the same prefix syntax.
What about the code-behind?
If you're wondering what must be done about any shared control code-behind that also needs to be cross-compilable then you'll be happy to know that the answer is much less complicated. Any C# code-behind that you use for your controls is already getting sent through a standard preprocessor so you can use "#if !SILVERLIGHT" blocks for any non-Silverlight code that you need to use. You would generally want to use this in places where a different namespace needs to be included or when you're interacting with a control that doesn't show up until it has been run through my XAML preprocessor. For example, you would use a block like the one below if you needed to directly interact with either the Silverlight or WPF versions of the DataGrid control in your code-behind.
#if SILVERLIGHTusing System.Windows.Controls;#elseusing Microsoft.Windows.Controls;#endif
Ideal workflow for multi-targeting shared XAML
Since this tool only performs one-way conversion from Silverlight-targeted XAML markup to WPF markup, a clear set of steps can be used for developing controls with Silverlight-incompatible XAML that need be shared on both platforms. This approach assumes that you would start by developing the control in Silverlight since it's the limiting factor for compatibility.
1. Design the control for Silverlight either by hand or in Expression Blend.
2. If the control is simple enough and requires no exceptions for WPF, try compiling it and running it in WPF.
3. If special cases are found for the WPF control (such as a dependency being in a different assembly, like the DataGrid), go back to the Silverlight version of the control and set up the ignorable "WPF" namespace as demonstrated earlier in this post.
4. Mark any incompatible elements or attributes with the "WPF" namespace and add any namespace additions/redefinitions to the root element of the control's XAML.
5. Add the Silverlight version of the XAML file and its code-behind as linked files in your WPF project so that you only have to perform edits once.
6. Use a build targets file (Ben Lemmon has a great example of one in his blog post) to automatically run the preprocessor on your WPF XAML files in the shared control project so that they automatically get converted before being compiled.
As the title of this section states, this is the ideal workflow. At this moment I don't have a solution for step 6 that handles linked files appropriately. I've been trying to work around this issue but haven't had any luck so far since I am no MSBuild expert. For now I've been using my own batch file to run the preprocessor on the XAML files. I realize that this is an important problem to solve before this tool can be used seamlessly in real projects. As soon as I find a viable solution to this problem I will post it here on my blog.
My preprocessor tool and included example code
I've attached my tool, its source, and example Silverlight and WPF projects in XAMLPreprocessor.zip. I'm releasing all of this code under the terms of the Ms-PL open source license so you can use it in your own applications. You can run the ProcessMainPage.bat file in XAMLProcessor\WPFTest to cause the shared MainPage.xaml file from the SilverlightTest project to be run through the preprocessor and saved back in the WPFTest folder. This XAML file demonstrates the preprocessor features that I described in this blog post and also serves as good place to experiment with some compatibility scenarios that you're dealing with in your own projects.
[Note: You will need to have Silverlight 3 Beta and the WPF Toolkit installed before you can compile these projects.]
Here is the XAML definition for the shared MainPage.xaml file that is used in my example solution:
<UserControl x:Class="SilverlightTest.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:WPF="WPF" mc:Ignorable="WPF" xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data" WPF:data="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit" WPF:controls="clr-namespace:System.Windows.Controls;assembly=PresentationFramework" Width="400" Height="300"> <Grid x:Name="LayoutRoot" Background="White" Margin="3"> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Grid.Resources> <DataTemplate x:Key="ProgressColumnTemplate"> <ProgressBar Value="{Binding Value}" Maximum="100" Margin="2"/> </DataTemplate> </Grid.Resources> <TextBlock Grid.Row="0" Width="100" HorizontalAlignment="Left" Text="Here is a bunch of sample text" TextWrapping="NoWrap" WPF:TextTrimming="WordEllipsis"/> <data:DataGrid Grid.Row="1" ItemsSource="{Binding TestList}" AutoGenerateColumns="False"> <data:DataGrid.Columns> <data:DataGridTextColumn Header="Name" Binding="{Binding Name}" /> <data:DataGridTextColumn Header="Value" Binding="{Binding Value}" /> <data:DataGridTemplateColumn Header="Progress" CellTemplate="{StaticResource ProgressColumnTemplate}" /> </data:DataGrid.Columns> </data:DataGrid> <WPF:controls_TextBlock Grid.Row="2" Text="This text only appears in WPF!" Foreground="Red" FontSize="14" FontWeight="Bold" /> </Grid></UserControl>
And here is a side-by-side screenshot of MainPage.xaml running on both platforms:
You can see that the TextBlock at the top of the Silverlight version (left) gets truncated while the one the the WPF version (right) has ellipses due to the TextTrimming property being used for that version of TextBlock. You can also see the usage of the DataGrid control in both versions, as shown in earlier XAML examples. The last thing to note is the extra TextBlock at the bottom of the WPF version doesn't appear in the Silverlight version. I don't imagine this will be a very common use case, but it is nice to have the ability to do so regardless.
If you have any feedback or improvement suggestions on the technique, please let me know!