Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Pluggable Styles and Resources in WPF with Language Converter Tool

0.00/5 (No votes)
31 Oct 2010 1  
In this article, I have shown how you can build pluggable Resources for styles, Languages or any static objects, etc. Therefore building a new style doesn't hamper your code and you can easily plugin any new style to the application even though it is already in the production environment.

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 Stylesand Themeswhich 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 ResourceDictionaryobjects 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.Reflectioncomes 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 Typehas 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 MainApplicationand the Resources The ResourceBaselibrary 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.

plugin1.JPG

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; }
}

IResourceAttributedefines 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 LocalizationResourceAttributeintroduces the CultureInfoobject. 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; }

        /// <summary>
        /// Defines the Color Base to be used
        /// </summary>
        public Color BaseColor { get; set; }
        public StyleResourceAttribute(string name)
        {
            this.Name = name;
        }
    } 

Here the BaseColorwill 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 = trueallows 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.

plugin2.JPG

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>

    <!--  Your code goes here -->
    <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> 

plugin3.JPG

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:Classas 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 InitializeComponentin its Constructor. So in other words, you need to create a custom class inherited from ResourceDicrtionaryand use InitializeComponentin 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 AssemblyInfofile 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 Assemblyto ensure that the assembly is actually a Style We will parse this attribute later on from our application and produce our actual assembly.

plugin4.JPG

ResourceClassPathplays a vital role. It made us understand where the actual Resource exists. So it is very important to specify the exact classPathfor 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 ResourceUtiland used app.config to load the Styledynamically 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()
        {
           //Get Styles Folder path
           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;
          // We can do this as we are fetching from AvailableStyles.
           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 LoadAssembliesactually 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.LoadAssemblieswill 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 Dictionaryobject 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 AvailableStylesand gives only the Resourceobject for which the currentThemespecified 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 BaseColorobject.

So finally, let's create a handler for Application.Startupand 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 ResourceDictionaryto 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 DynamicResourcerather than StaticResource StaticResourcetries 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 StaticResourceelements to DynamicResourceand 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.

plugin5.JPG

Hence you can see the Styleis changed when you change the CurrentThemekey of your app.config to SilverRedto 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 ResourceDictionaryusing 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 ResourceUtilto return appropriate ResourceDictionaryfor Current Language settings from the user.

So that ResourceDictionarywill 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 FrenchResourceDictionaryin 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 DynamicResourcesfor 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 ResourceUtillanguage method looks like:

public static ResourceDictionary GetAppropriateLanguage()
        {
            //Get Language Folder path
            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:

plugin8.JPG

So the application looks like:

plugin9.JPG

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.

plugin6.JPG

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.

plugin7.JPG

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here