It’s no secret that Silverlight and WPF share a lot in common, primarily a result of their shared CLR Framework heritage. I have noticed a considerable increase in the number of organizations that are building applications that target both platforms. And in particular I have seen a rise in the number of organizations that are building a single application for both platforms while sharing as much code between them as possible. The results vary, some are more successful than others.
Far and away the most common approach I see is the use of the Linked Files feature in Visual Studio. We create a new Class Library project and within it we write code normally. Then a Silverlight Class Library project is added to the solution and we simply add all of the *.cs files from the normal Class Library projects using the “Add Existing Item… Add As Linked” process within Visual Studio. We typically use conditional directives (#if SILVERLIGHT) to handle small variances between the two platforms. When we build our solution we get two assemblies – one for desktop/WPF applications and one for Silverlight applications. Pretty straightforward and easy to do.
However there are some drawbacks to this.
- First, this technique is incompatible with wildcard inclusions, which was a major hurdle for a recent project of mine.
- Second, the compiler itself seems to have trouble with Linked Files. When a compiler error is encountered, you can double-click the error to jump directly to the source line. However Visual Studio doesn’t know if you want to go to the original file or the linked file. This matters because the linked version will have different assembly references and conditional #defines. Visual Studio has a 50/50 chance of picking the right one, but in my experience it almost always guesses poorly.
So in this article I would like to show a different approach that works around those issues. Like my prior article on Wildcard Inclusions, this approach will require manual project file editing since it leverages features of msbuild that are not surfaced adequately through the Visual Studio UI.
Let’s get started.
Using Custom Build Targets
This approach leverages the Custom Build Targets feature of Visual Studio. I would be willing to bet that most developers are fully unaware of it aside from the occasional switching between the default Debug and Release on their build toolbar. And let’s be honest here. I would bet that more than 50% of developers never even build to anything except for the Debug target (even when creating a Production build for deployment).
Instead of adding a new “Silverlight” project with Linked File references, we will use a single project file and distinct “DesktopDebug/DesktopRelease” and “SilverlightDebug/SilverlightRelease” build targets to handle the builds for each platform. The first issue I listed above will be resolved because we are no longer using Linked File Items in the project file. And the second issue will no longer be a problem because Visual Studio will not be confused by having multiple ways to reach the same physical source file from different projects.
Start with a Desktop CLR Project
To demonstrate this technique we can create a very simple starter project. Obviously, a real scenario would include considerably more code than this.
First, create new Windows C# Class Library, names of the project and solution container are unimportant. For this example I chose “MyClassLibrary”. This project template creates a default source code file named Class1.cs with a stub for Class1. This is fine for our purposes here, so let’s insert some code to demonstrate that our dual-target configuration is working properly. We can do this by using a conditional directive to include/exclude code based on the presence of the “SILVERLIGHT” conditional. This conditional is only defined when compiling for a Silverlight target environment.
When a Silverlight build target is active the first version of the Foo constant will be compiled. When a Desktop build target is active, the second version will be compiled. This should be simple enough to demonstrate the concept.
Modify the Build Target Configuration
Select “Configuration Manager” from the build target dropdown on your toolbar to launch the Configuration Manager dialog:
Select “<Edit…>” from the Active Solution Configurations dropdown. This will display the Edit Solution Configurations dialog where you can Rename the existing configurations.
Rename “Debug” to “DesktopDebug” and rename “Release” to “DesktopRelease”. These are the build configurations you will want to use when debugging Desktop-specific code.
Next, we want to add the new Build Configurations to support Silverlight compilation. Under the Active Solution Configurations dropdown select the “<New…>” option to bring up the New Solution Configuration dialog. First we will create the SilverlightDebug configuration, so enter that for the Name field. For the Copy Settings From option, select DesktopDebug from the list. Be sure to check the “Create new project configurations” option. This will enable each configuration to use a different set of references and conditionals. Follow the same steps to copy the DesktopRelease settings to a new SilverlightRelease configuration:
We should now see all four configurations listed in Visual Studio’s Build Toolbar:
You will use this dropdown to switch build output targets when debugging/compiling.
There is one more step to be performed using the Visual Studio configuration tools. Right-click the project node in Solution Explorer and bring up the Properties page for our Class Library project. Alternatively, you could select “MyClassLibrary Properties…” from the Project menu. On the Build tab you will see that there are no Conditional Compilation Symbols defined (the setting is blank). Use the Build Configuration dropdown to switch to the SilverlightDebug build target. Then enter “SILVERLIGHT” in the Conditional Compilation Symbols textbox. Switch again to SilverlightRelease and enter the same value. You should be able to see that this value is unchanged for the Desktop builds by switching to those build targets too (the Properties page updates when you select a new build target).
Note that you may also want to customize the Build Output Path for the Silverlight build targets so that they get directed to a different output folder when switching build targets. Otherwise the Silverlight output will overwrite the Desktop output and vice versa. It just keeps things ever so slightly more tidy.
That is all we need in order to set up the environment. However there is still one more task to perform, and unfortunately the Visual Studio IDE will not help us with this part. We need to configure our project to use the Silverlight Core assemblies when compiling to the Silverlight build targets.
Modify the Project File to use Silverlight References
Right-click the project node in Visual Studio’s Solution Explorer pane and select “Unload Project” (or select “Unload Project” from the Project menu). The project for MyClassLibrary should now show up as unavailable. Right-click it again, and select “Edit MyClassLibrary.csproj” this time to open up the project source file directly in Visual Studio’s XML editor.
The first thing we need to do here is apply some magic fairy dust. MSBuild will automatically pull in references to essential CLR assemblies (such as System.Core), but we don’t want this behavior for our Silverlight build targets which need to reference a different version tailored for that runtime. This is not very hard to do. At the very top of your project file, just insert the following PropertyGroup XML element, as the first child element of the Project element:
The next thing we want to do is create some substitution variables that point to your Silverlight installation folder and your SDK folder. These paths will be used by the compiler when building your assembly (these paths are only used by the compiler to find the right DLLs to build against). Be sure to update these paths if you have installed them to another location. On my system these are in the default locations, but your system might be different, so verify the path values. Also, it is possible – likely even – that in a real build scenario you might want these paths pointing to a folder that is itself under source control. In that case you would use the substitution variable $(MSBuildThisFileDirectory) in the path to have msbuild figure out the absolute path for you. For this example I am just hard-coding the SDK and Runtime paths:
This PropertyGroup should be placed after the first element that we inserted in the prior step.
In addition to setting up those substitution variables, in the above XML we are also setting a new variable named “SilverlightBuild” to “true” and preventing the compiler from including standard libraries. Note the use of a Conditional that only sets these options when the current Configuration is set to SilverlightRelease or SilverlightDebug.
After setting up these substitution variables and other build options, the only thing left is to tell the compiler which assemblies we really want it to use instead of the defaults. Scroll down towards the bottom of the project file and you will find an ItemGroup element that contains a number of Reference elements. These are not appropriate for Silverlight, so remove that ItemGroup section and replace it with the following two sections (the first will be used when compiling to Desktop, the second when compiling to Silverlight):
That’s all there is to it.
Testing and Verifying Our Changes
With our configuration profiles in place we can verify that everything is working properly. Simply switch to one of the Desktop build configurations and rebuild the solution. You will notice that immediately upon switching configurations the code editor window for Class1 will recognize the change in conditional symbols. Also, the Build Output window will show that the standard assemblies were referenced by csc.exe when compiling the project.
Switch to one of the Silverlight build configurations and you will again notice that the Class1 editor immediately reflects the change in conditional symbols (the other half of the conditional section becomes active). Additionally, you will notice in the Build Output window that the Silverlight assemblies were referenced by csc.exe during compilation.
So there you have it – a more robust way to handle dual-target source trees. While it requires you to exert a small amount of effort with a few manual edits of the project file, in the end it is usually less hassle than constantly updating Item Links and dealing with an IDE that never seems to load the correct file when navigating to error lines.
If you have any trouble with this, I have uploaded my example project used to write up this article here for reference:
Observant readers might have noticed that this technique adds a build variable named SilverlightBuild, which is valid as a Condition for any ItemGroup, including source files themselves. If you have entire source files that need to be included/excluded under Silverlight, then you can use this functionality instead of sprinkling “#if SILVERLIGHT” throughout your source code. In the example project source I have done exactly that, and included a class named SilverlightOnlyClass that is only compiled when the build target is SilverlightDebug or SilverlightRelease.