Gigi Labs

Please follow Gigi Labs for the latest articles. Programmer's Ranch no longer has its domain, so please update your bookmarks and links to programmersranch.blogspot.com.

Wednesday, October 9, 2013

C# WPF: Control Panel using MVVM

Hi all! :)

In this article I'm going to show how you can implement a simple control panel in WPF using the MVVM design pattern. A control panel is a special case of a master/detail layout, where options are usually few and predefined, so you don't need to load any detail dynamically as options are selected.

This is an advanced article, so read on only if you have a fair grasp of the following: C#, WPF, data binding, and MVVM. I'm going to be using Visual Studio 2013 RC for the examples (just to try it out, really), but you should be able to use SharpDevelop or any version of Visual Studio from 2008 onwards.

Before we begin, a brief note on MVVM is in order. MVVM (Model-View-ViewModel) is an approach that promotes using data binding and commands over events. What we're going to do in this article can easily be done using the event-driven model (a la Windows Forms), but I'm writing this mainly for those who are trying to adopt the MVVM approach and who (like me) had a hard time grasping how it works.

So let's start off by adding a new C# WPF Application:


The New Project window above is an example of a master/detail layout. As you click on items in the treeview on the left (e.g. Windows or Web), the list of items in the middle changes accordingly. The treeview is a master view, while the area in the middle is the detail view.

For starters, in the XAML view, we can do away with the default <Grid> and set up our (not yet functional) master/detail layout as follows:

    <DockPanel>
        <ListBox Width="150" DockPanel.Dock="Left" />
        <ContentControl />
    </DockPanel>

Here's a screenshot of what it looks like in Visual Studio:


Isn't that simple? We use a ListBox for the master view, and move it to the left thanks to the DockPanel that contains it. The detail view is a ContentControl, which is a container we can use to hold user controls.

Next, we need a ViewModel for our MainWindow. Add a new class (right click on project, select Add -> New Item...) and call it MainVM. Make the class public, and set up some hardcoded values for the options that will appear in the master view:

    public class MainVM
    {
        private List<String> operations;

        public List<String> Operations
        {
            get
            {
                return this.operations;
            }
        }

        public MainVM()
        {
            this.operations = new List<String>() { "Add User", "List Users" };
        }
    }

In MainWindow's codebehind (i.e. MainWindow.xaml.cs), set the DataContext to an instance of MainVM in the constructor. This is important because when we do our data binding, the properties are always relative to the DataContext.

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            this.DataContext = new MainVM();
        }
    }

Back in our XAML view for MainWindow.xaml, we can now see that our binding works by adding an ItemsSource to the master ListBox:

<ListBox Width="150" DockPanel.Dock="Left" ItemsSource="{Binding Path=Operations}" />

Note how we're binding to the Operations property, and this is interpreted as belonging to the DataContext, which is a MainVM in this case. Sure enough, pressing F5 to debug the project shows that the ListBox is filled with the values we hardcoded earlier:


Right, now to actually fill in the detail view depending on what master option is selected. We'll first create each detail as a separate UserControl - this will allow us to easily plug them into our window.

Add your first UserControl (right click on project, Add -> New Item... and select "User Control (WPF)" - don't confuse it for the "User Control" which is actually a Windows Forms thing) and name it AddUserControl.xaml. Instead of implementing functionality to add or list users (which isn't the point of this article, we'll just stick some static content instead. Put this instead of the default grid:

    <StackPanel>
        <StackPanel Orientation="Horizontal">
            <TextBlock VerticalAlignment="Center">Name:</TextBlock>
            <TextBox Width="100" Margin="5 5 5 5" />
        </StackPanel>
    </StackPanel>

Create another UserControl, ListUsersControl.xaml, and replace the grid with the following:

    <StackPanel>
        <TextBlock>John Doe</TextBlock>
        <TextBlock>Mary Jane</TextBlock>
        <TextBlock>John Smith</TextBlock>
        <TextBlock>Chuck Norris</TextBlock>
    </StackPanel>

Now, we need to map each operation name (e.g. "Add User") to the corresponding UserControl. One option is to use a Tuple, but a better solution is to create a dedicated class. Add a new class named Operation and implement it like this:

    public class Operation
    {
        public String Name { get; set; }
        public UserControl Control { get; set; }

        public Operation(String name, UserControl control)
        {
            this.Name = name;
            this.Control = control;
        }
    }

You will also need to add the following near the top for UserControl to make sense:

using System.Windows.Controls;

Back in MainVM, replace the whole (String-based) class with the following (Operation-based):

    public class MainVM
    {
        private List<Operation> operations;

        public List<Operation> Operations
        {
            get
            {
                return this.operations;
            }
        }

        public MainVM()
        {
            this.operations = new List<Operation>();
            this.operations.Add(new Operation("Add User", new AddUserControl()));
            this.operations.Add(new Operation("List Users", new ListUsersControl()));
        }
    }

All we have left to do is wire up the bindings on the XAML view of MainWindow.xaml. First, we need to change our ListBox as follows:

        <ListBox Name="MasterView" Width="150" DockPanel.Dock="Left"
                 ItemsSource="{Binding Path=Operations}" DisplayMemberPath="Name" />

I've added two things here. First, I added a name to the ListBox so that I can refer to it from my ContentControl. I also added a DisplayMemberPath. This tells the ListBox that for each Operation item, it needs to show the Name property. If you leave that out, it will just display Operation.ToString() by default.

Next, we bind our ContentControl's Content property as follows:

        <ContentControl Content="{Binding ElementName=MasterView, Path=SelectedItem.Control}" />

The binding refers to the control named MasterView (i.e. our ListBox), and we hook it up with the ListBox's SelectedItem property. Since the SelectedItem is actually an Operation, we get the UserControl associated with that Operation (via the Control property). Setting the Path to SelectedItem.Control does the trick.

That's it! Press F5 to test the application. When you click on "Add User", you get the correct UserControl:


...and clicking on "List Users" works just as well:


Woohoo! :) We managed to implement an MVVM master/detail control panel with just three lines of XAML (two controls, really) and some data binding magic.

As you have seen, the particular nature of a control panel makes an MVVM implementation particularly easy, because each detail view can reside in memory. This does not apply to all master/detail layouts though: if the master list is really long, it may be a better idea to load detail views lazily on demand. That could be the topic for a future article. Stay tuned! :)

No comments:

Post a Comment