Introduction
As we go ahead with WPF, there are lots of problems we face which are very basic but with lots of importance. In my last application with WPF, I found that it is very essential to build a solid foundation to Styles
and Themes
which we can use in our application. While we build our application, we create resources. Some of them we place in resource dictionary while we place others in the window itself. Thus when we finally release our code, we find that changing the theme is to be a huge task altogether. In this article, I will discuss the steps how you can easily manipulate styles by placing the ResourceDictionary
objects into another library and use .NET Reflection to plugin the Resource directly into your application.
A Note on Reflection
Reflection is a very important part of any Windows application. We need reflection to call objects which are not directly referenced with the project. Keeping a strong reference between two assemblies often makes it tightly coupled with one another. That means the two components are totally dependent between one another and an individual element cannot exist without the other one.
Reflection allows you to read a DLL from any location by specifying the UNC path and allows you to create objects and call methods. System.Reflection
comes with Classes like Assembly
Type
Module
MemberInfo
PropertyInfo
ConstructorInfo
FieldInfo
EventInfo
ParameterInfo
Enum
etc. to invoke various functionality of any .NET objects. For instance:
Assembly thisAssembly = Assembly.LoadFile(fi.FullName);
var object = thisAssembly.CreateInstance("MyType");
Thus the object will hold the instance of MyType
Similar to that, each Type
has methods which gets you all MethodInfo
FieldInfo
PropertyInfo
etc. which you can invoke through the object created as above and do your work.
In this article, we will add few lines from Reflection to plugin styles and languages from a specific folder. You can read more about Reflection from MSDN Reflection.
Implementation of Attributes
As we are going to reference external DLLs from our application, it is very essential to define the entry point for each external entity. To define the external entity, I create a class library which mediates between the MainApplication
and the Resources
The ResourceBase
library will define few Attributes which will later be used to invoke members from the DLLs.
It is to be noted that we will create resources as separate DLLs. These attributes will allow us to get meta data of the DLL itself.
To make each of the Resources compatible, I have created an Interface:
public interface IResourceAttribute
{
string Name { get; set; }
string Description { get; set; }
string ResourceClassPath { get; set; }
}
IResourceAttribute
defines three properties. Name which we will use to call the Resource, the Description of the Resource and ResourceClassPath
which is very important to identify the path of the class which makes the appropriate resource within the Assembly.
Now let us create a Concrete Attribute that lets us input the metadata specific to each type.
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple=true, Inherited=true)]
public class LocalizationResourceAttribute : Attribute, IResourceAttribute
{
public CultureInfo Culture { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string ResourceClassPath { get; set; }
public LocalizationResourceAttribute(string culture)
{
this.Culture = new CultureInfo(culture);
}
}
Here you can see the LocalizationResourceAttribute
introduces the CultureInfo
object. This will let you define culture specific to the current culture of the application.
Similar to this:
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = true)]
public class StyleResourceAttribute : Attribute, IResourceAttribute
{
public string Name { get; set; }
public string Description { get; set; }
public string ResourceClassPath { get; set; }
public Color BaseColor { get; set; }
public StyleResourceAttribute(string name)
{
this.Name = name;
}
}
Here the BaseColor
will allow your DLL to expose the color base for default application.
Note that we use AttributeTargets.Assembly
as we need the attribute to be present in the Assembly level. AllowMultiple = true
allows you to create more than one Resource in the same assembly. As such, we can have more than 1 style in the same DLL.
Implementation of Styles
Now as we are ready to go, let us try create a few styles and see how it looks on the application. To start, let's create a new Class library and take reference to PresentationCore.dll, PresentationFramework.dll and WindowsBase.dll as it is required explicitly for any application.
Note: You also need to add the DLLs which you want to reference from your styles. Like if you need WPFToolKit, you can go ahead here to do that.
Next, you need to add up the Custom DLL that we have just produced. You can see in the image above that I have added my own custom ResourceBase.dll which we will use to mark the assembly with special attributes.
Now it's time to implement a style for your application.
<ResourceDictionary x:Class="GoldenBlack.GoldResource"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Custom="http://schemas.microsoft.com/wpf/2008/toolkit">
<Color x:Key="ShadeHover">#F9E4B7</Color>
<Color x:Key="ShadeBack">#F48519</Color>
<Color x:Key="ShadeHighlight">#F9BE58</Color>
-->
<Style TargetType="{x:Type TextBlock}"
x:Key="tbBasic">
<Setter Property="FontFamily"
Value="Calibri" />
<Setter Property="FontSize"
Value="18"></Setter>
<Setter Property="ScrollViewer.CanContentScroll"
Value="true" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility"
Value="Auto" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility"
Value="Auto" />
<Setter Property="Foreground"
Value="{StaticResource ShadeHighlightBrush}"></Setter>
</Style>
</ResourceDictionary>
After you are done with creating your custom Resource, you need to create a class. Just from the solution explorer, add a new Class file and make it public
In your Resource file, you need to add x:Class="YourNamespace.Yourclass"
Thus you need to add the x:Class
as the exact logical path of the class. In my case, it is x:Class="GoldenBlack.GoldResource"
So the class will look like:
namespace GoldenBlack
{
public partial class GoldResource : ResourceDictionary
{
public GoldResource()
{
InitializeComponent();
}
}
}
Actually while adding up any resource, .NET implicitly creates a class for it and then adds it. As we need to do it manually using reflection, you need to add the custom class and add InitializeComponent
in its Constructor. So in other words, you need to create a custom class inherited from ResourceDicrtionary
and use InitializeComponent
in its default constructor.
So finally, it's time to compile and produce an assembly which you could use for your main application.
Before you do, you need to add a few lines in AssemblyInfo
file that you can find inside Properties folder.
[assembly: StyleResourceAttribute("GoldenBlack",
Description = "Theme with Black and Gold",
ResourceClassPath = "GoldenBlack.GoldResource")]
This will add a special attribute as meta data of the Assembly
to ensure that the assembly is actually a Style
We will parse this attribute later on from our application and produce our actual assembly.
ResourceClassPath
plays a vital role. It made us understand where the actual Resource exists. So it is very important to specify the exact classPath
for the Resource in the library.
Note: I have use AllowMultiple=true, which will enable you to add more than one Style into the same assembly.
Creating the Main Application
Now it's time to go to the main application and see how to apply styles dynamically. For simplicity, I have added a new class called ResourceUtil
and used app.config to load the Style
dynamically when the program loads.
public static class ResourceUtil
{
public static Dictionary<IResourceAttribute, Assembly> AvailableStyles =
new Dictionary<IResourceAttribute, Assembly>();
public static Color BaseColor { get; set; }
public static ResourceDictionary GetAppropriateDictionary()
{
string path = ConfigurationSettings.AppSettings["stylefolderpath"];
string currentTheme = ConfigurationSettings.AppSettings["CurrentTheme"];
ResourceUtil.LoadAssemblies(AvailableStyles, path);
IResourceAttribute currentResource =
AvailableStyles.Keys.FirstOrDefault<IResourceAttribute>(item =>
item.Name.Equals(currentTheme));
StyleResourrceAttribute sra= currentResource as StyleResourceAttribute;
if(sra != null)
BaseColor = sra.BaseColor;
if (currentResource != null)
{
Assembly currentAssembly = AvailableStyles[currentResource];
Type resourceType =
currentAssembly.GetType(currentResource.ResourceClassPath);
ConstructorInfo cinfo = resourceType.GetConstructor(Type.EmptyTypes);
ResourceDictionary dictionary = cinfo.Invoke(new object[] { })
as ResourceDictionary;
return dictionary;
}
return null;
}
private static void LoadAssemblies(Dictionary<IResourceAttribute, Assembly>
resource, string path)
{
DirectoryInfo di = new DirectoryInfo(Path.Combine(
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), path));
foreach (FileInfo fi in di.GetFiles())
{
try
{
Assembly thisAssembly = Assembly.LoadFile(fi.FullName);
var attributes = thisAssembly.GetCustomAttributes(true);
IEnumerable<object> resourceAttributes =
attributes.Where(item => item is IResourceAttribute);
foreach (IResourceAttribute raatr in resourceAttributes)
AvailableStyles.Add(raatr, thisAssembly);
}
catch { }
}
}
}
Here in the code above, you can see the Application LoadAssemblies
actually loads an assembly from the provided folder path. Thus in our case, we load all the assemblies from the folder specified explicitly for Styles. So ResourceUtil.LoadAssemblies
will load all the assemblies within the folder specified as path to AvailableStyles
Now it's time to invoke the Resource and get an object of ResourceDictionary
As the actual Dictionary
object is not present with us now as we didn't have strong reference to the loaded assembly, we use Reflection for this purpose.
IResourceAttribute currentResource =
AvailableStyles.Keys.FirstOrDefault<IResourceAttribute>(item =>
item.Name.Equals(currentTheme));
The above line filters out all the assemblies loaded in AvailableStyles
and gives only the Resource
object for which the currentTheme
specified within the app.config matches.
As the attribute also has a BaseColor
we need to add that functionality too. So we place the color to the BaseColor
object.
So finally, let's create a handler for Application.Startup
and place few lines to load the Dictionary
public partial class App : Application
{
private void Application_Startup(object sender, StartupEventArgs e)
{
ResourceDictionary dictionary = ResourceUtil.GetAppropriateDictionary();
if (dictionary != null)
this.Resources.MergedDictionaries.Add(dictionary);
}
}
So this would add the new ResourceDictionary
to the Application. Hence the styles are applied.
Wait wait, this is not the end. You also need to make a few adjustments to your application. Means you can reference the styles only by using DynamicResource
rather than StaticResource
StaticResource
tries to find the resources during compile time, and thus in our case it will not find it there. So our sample code will look like:
<Window x:Class="MainWpfApplication.MyMainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MyMainWindow"
Background="{DynamicResource ShadeFadedBackgroundBrush}">
<Grid>
<DockPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock DockPanel.Dock="Top" MinWidth="400"
Style="{DynamicResource tbBasic}" Text="This is my custom Text"/>
<Button Content="Accept" Click="Button_Click"
DockPanel.Dock="Top" Style="{DynamicResource btnBasic}"/>
</DockPanel>
</Grid>
</Window>
You can see that I have replaced all the StaticResource
elements to DynamicResource
and hence we open the ability to change the styles at runtime.
Now, place the DLLs to the application directory as specified in app.config and run the application.
Hence you can see the Style
is changed when you change the CurrentTheme
key of your app.config to SilverRed
to GoldenBlack
Voila, we are done with it.
You can load the Resources dynamically if you want. To do so, you need to hold the current Resource which is added to the application, and then remove the current theme and add the ResourceDictionary
using the following code:
((App)Application.Current).Resources.MergedDictionaries.Add(dictionary);
Thus, you can easily make the application dynamically load the resources based on users interaction.
Working with Other Resources
This is not the end of this. Few days before, I have introduced a way to take resources as a technique of multilingual application. If you don't remember, you can try it from:
So let's extend this with plugin based language application.
Working almost in the similar way, we add a new method to ResourceUtil
to return appropriate ResourceDictionary
for Current Language settings from the user.
So that ResourceDictionary
will look like:
<ResourceDictionary x:Class="LanguageResource.EnglishResource"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="rKeychangetheme" >Change Theme</system:String>
<system:String x:Key="rKeycustomtext" >This is my custom Text</system:String>
<system:String x:Key="rKeyaccept" >Accept</system:String>
</ResourceDictionary>
Similar to that, we add a French Dictionary. To show you how you can use multiple resources in the same library, I have added the FrenchResourceDictionary
in the same folder:
<ResourceDictionary x:Class="LanguageResource.FrenchResource"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="rKeychangetheme">Changer de thème</system:String>
<system:String x:Key="rKeycustomtext">C'est mon texte personnalisé</system:String>
<system:String x:Key="rKeyaccept">Accepter</system:String>
</ResourceDictionary>
You can notice that the keys are all maintained in the same way, while the Values are modified. Now it's time to compile the assembly. Before doing that, let's add the custom attributes to AssemblyInfo.cs file of the project.
[assembly: LocalizationResource("en-US",
Name = "English dictionary", Description = "For English Dictionary",
ResourceClassPath = "LanguageResource.EnglishResource")]
[assembly: LocalizationResource("fr-Fr",
Name = "French dictionary", Description = "For French Dictionary",
ResourceClassPath = "LanguageResource.FrenchResource")]
I have added both the resources to the same DLL, so you have to add both of them to the AssemblyInfo
We will load each of them to the main application later.
Now similar to this, let's modify the main XAML code with DynamicResources
for the string
:
<Grid>
<DockPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock DockPanel.Dock="Top" FontSize="20"
Style="{DynamicResource tbBasic}" x:Name="tbCurrentTheme" />
<TextBlock DockPanel.Dock="Top" MinWidth="400"
Style="{DynamicResource tbBasic}"
Text="{DynamicResource rKeycustomtext}"/>
<Button Content="{DynamicResource rKeyaccept}"
Click="Button_Click" DockPanel.Dock="Top"
Style="{DynamicResource btnBasic}"/>
</DockPanel>
</Grid>
So the slight alteration to ResourceUtil
language method looks like:
public static ResourceDictionary GetAppropriateLanguage()
{
string path = ConfigurationSettings.AppSettings["languagefolderpath"];
CultureInfo currentCulture = Thread.CurrentThread.CurrentCulture;
ResourceUtil.LoadAssemblies(AvailableDictionaries, path);
IResourceAttribute currentResource =
AvailableDictionaries.Keys.FirstOrDefault<IResourceAttribute>(
item =>
{
LocalizationResourceAttribute la = item as
LocalizationResourceAttribute;
if (la != null)
return la.Culture.Equals(currentCulture);
return false;
});
if (currentResource != null)
{
Assembly currentAssembly = AvailableDictionaries[currentResource];
Type resourceType =
currentAssembly.GetType(currentResource.ResourceClassPath);
ConstructorInfo cinfo = resourceType.GetConstructor(Type.EmptyTypes);
ResourceDictionary dictionary = cinfo.Invoke(new object[] { })
as ResourceDictionary;
return dictionary;
}
return null;
}
Thus, we load the languages according to the Regional settings. You can change the logic according to what suits you.
To change the language settings, you can try:
So the application looks like:
So you can see the Text is modified according to the languages added to the application. Similar to this, the Object Resource can also be plugged in.
Language Tool
Creating Language resources often comes to me as very boring. So I thought it would be nice to give you a tool which converts one Resource to another. So, if you have built only one string Resource and want to give support for multiple resources to your customers, try my Language converter to generate Resource files for you.
You can find the language converter tool with full source code from ResourceConverter For Language using Bing translator, or read my blog post Resource Generator Tool for WPF.
After you run the application, you will find the UI something like what is shown above. You need to choose the Target file, which we have to build. Let me create the English resource first, and choose the target as the same file. The English file looks like:
<ResourceDictionary x:Class="LanguageResource.EnglishResource"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="rKeychangetheme" >Change Theme</system:String>
<system:String x:Key="rKeycustomtext" >This is my custom Text</system:String>
<system:String x:Key="rKeyaccept" >Accept</system:String>
</ResourceDictionary>
In the destination, you need to specify a name which the converter will convert to, and click Convert.
The resource will be converter instantly to French, and keys will remain the same.
<ResourceDictionary x:Class="LanguageResource.FrenchResource"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="rKeychangetheme">Changer de thème</system:String>
<system:String x:Key="rKeycustomtext">C'est mon texte personnalisé</system:String>
<system:String x:Key="rKeyaccept">Accepter</system:String>
</ResourceDictionary>
So this is what we needed for the application. I have added all the supported languages from Bing translator to this tool, so that you can change resources from any language to any other.
To know more about this tool, please go ahead and read Resource Generator Tool for WPF.
Conclusion
I think Plug-gable resources is what every application needs. We build applications long before we need styles and Resources. Functionality is the primary thing for any application. But following these basic guidelines will make you add plug-gable themes very easily.
I hope this article will help you. Thank you for reading.