Programming and Computing facts by Mohammed Abdulsattar

Monday, February 8, 2010

Ultimate Theme Support in WPF MVVM Applications Using MEF

In MVVM, the Model or the ViewModel know nothing about the View making it pretty easy to change the View. But changing it after building your application is not what you can do with WPF alone (as far as I know). You can achieve this using Managed Extensibility Framework very easily. I've seen many applications providing theme support by including a various number of Resource Dictionaries. But this wouldn't modify the look and feel upside down. Using MEF, you can completely change the View of the application, not just the Resource Dictionaries.

I will use the the application that we built in my previous post, Creating a Complete Tabbed Inteface in WPF using MVVM. If you have read it or are confident that you can understand what it does, just by looking at source code ( it's very easy if you know a little about MVVM and WPF ), you can download it here and use it for this post. But if you don't think you can get it, I strongly recommend you read it.

Let's look at how we are going to implement it. We are going to have a folder called Themes which will have a lot of binaries (.dll) each representing a theme. Our application gets all the binaries when it starts and loads the theme, the name of which matches with the one stored in the application's settings. To achieve this, we are going to move the themes into a separate project.

Create a new WPF User Control library project (in the same solution) and name it DefaultTheme. Move the MainView.xaml to the DefaultTheme project and change the corresponding namespaces. Our code is still messed up, it won't compile. For this to run, the App class needs a MainView. We are going to give it using MEF. Add a reference the System.ComponentModel.Composition assembly from MEF to the TabbedLayout project. If you don't have it, download it at the CodePlex site of MEF. Now delete the App.xaml file; we are going to create our own. If you don't want to delete it, you can modify it and get our app running. But for this post, let's say you've deleted it. Create a new class App that derives from the Application class. Add the Main method that runs it.



class App : Application

{

    public static void Main(string[] args)

    {

        App app = new App();

        app.Run();

    }

}
Our application now runs (provided you've changed the namespaces correctly). But it does not show any window. To show a window, it needs a theme. By theme, we mean a name of the theme and a Window that can be used as the MainWindow of our application. Let's define an interface ITheme.




public interface ITheme

{

    public string Name { get; }

    public Window MainView { get; }

}
We need to implement this in the DefaultTheme so that it can be used in our application. Create a class DefaultTheme in the DefaultTheme project that implements the ITheme interface.




public class DefaultTheme : ITheme

{

    private MainView _mainView;

    public string Name

    {

        get { return "Default"; }

    }



public Window MainView

    {

        get { return _mainView ?? (_mainView = new MainView()); }

    }

}
Now, our application needs a theme. But, since our Themes folder can have a lot of themes in it, it would be better if we have a collection of Themes. A SelectedTheme will be used to launch the application.




public IEnumerable<ITheme> Themes { get; set; }

private ITheme _selectedTheme;
Now, we need to get the name of the theme from the settings of the application. Let's add a method Configure that does that and lauches the MainWindow.



[STAThread]

public static void Main(string[] args)

{

    App app = new App();

    app.Configure();

    app.Run();

}



private void Configure()

{

    if (Themes.Count() == 0)

    {

        MessageBox.Show("No theme is present. Application is shutting down");

        Shutdown();

        return;

    }



    string themeName = TabbedLayout.Properties.Settings.Default["ThemeName"] as string; //Get the theme name from settings

    _selectedTheme = Themes.SingleOrDefault(x =&gt; x.Name.Equals(themeName, StringComparison.OrdinalIgnoreCase)); //Get the theme with the above name from the collection



    if (_selectedTheme == null) //If there is no such theme, use the first theme in the collection

    {

        MessageBox.Show("The selected theme has been modified or removed. IncEditor will start with one of your available themes. You need to restart the application");

        _selectedTheme = Themes.First(x =&gt; true);

        TabbedLayout.Properties.Settings.Default["ThemeName"] = _selectedTheme.Name;

    }



    _selectedTheme.MainView.DataContext = new MainViewModel();

    MainWindow = _selectedTheme.MainView; //Set the MainWindow of the Application to the MainWindow of the selected theme

    _selectedTheme.MainView.Show();

}
We need to set the ThemeName setting in our application. Right Click the TabbedLayout project in the Solution Explorer and select Properties. Add a new User setting ThemeName and set its value to Default. We are going to use the Default theme.

Now our application is all ready, except that it does not have the Themes collection. We haven't initialized it yet. Now, we need MEF to load all the assemblies from the Themes folder and add all the IThemes to the collection. To do this, we need to add an ImportManyAttribute on the Themes collection in the App class.



[ImportMany(typeof(ITheme))]

public IEnumerable<ITheme> Themes { get; set; }
We need to export the DefaultTheme class from the DefaultTheme project. Add an



[Export(typeof(ITheme))]

public class DefaultTheme : ITheme
Now we need to do the MEF stuff, create a container, add a catalog to it and compose the parts. If you don't know what this is, refer to the Documentation on the Codeplex site of MEF. You need to create a folder Themes in the TabbedLayout project.



private void Compose()

{

    var catalog = new DirectoryCatalog(@"..\..\Themes");

    var container = new CompositionContainer(catalog);

    container.ComposeParts(this);

}
Call this method before Configure.



private void Configure()

{

    Compose();
Now run the application and you will see a MessageBox popping up saying no theme is present and the application shuts down. It's reasonable because you don't have any theme in the Themes folder. Now all you need to create is copy the DefaultTheme.dll into the themes folder and run the application again. This time it runs perfectly well as if nothing ever happened. But you've added great functionality to it. You can create another theme, add it to the Themes folder, change the ThemeName property and run your application using that theme. You can even display a list of all the themes available to the user and allow him to change it. I've done it in an open source project IncEditor. It would be very nice of you if you join it and give your valuable ideas so that nutters like me can increase our knowledge and brag about on blogs as I've been doing throughout this post. Wishing you good luck!

1 comment:

  1. I found this on internet and it is really very nice.
    An excellent blog.
    Great work!

    ReplyDelete

About Me

My photo
I'm a computer science engineering student with a lot of passion for computers.