Gigi Labs

Please follow Gigi Labs for the latest articles.
Showing posts with label datacontext. Show all posts
Showing posts with label datacontext. Show all posts

Thursday, October 17, 2013

C# WPF: Master/Detail Layout using MVVM

Welcome! :)

My recent article "C# WPF: Control Panel using MVVM" showed how you can make a specific master/detail layout suited for control panels - you select an option from the sidebar, and the interface on the right changes based on it. As I mentioned in the article, this approach is okay when the options are few and predefined. If you're working with displaying actual data, though, you need to work a little differently.

This is another advanced article dealing with MVVM, so prerequisites to understand the content include knowing C#, WPF, data binding, and a basic idea of MVVM. You will also need to download and install MVVM Light - a popular, lightweight MVVM library which we also used in yesterday's article to get some common MVVM functionality out of the box.

Let's start off by creating a new WPF application in SharpDevelop (or Visual Studio, if you prefer). Once that is done, right click on the project and select "Add Reference". From the ".NET Assembly Browser" tab, use the "Browse..." button to locate the "Mvvm Light Toolkit\Binaries\WPF4GalaSoft.MvvmLight.WPF4.dll" file from your MVVM Light installation. These instructions pertain to SharpDevelop using .NET 4, so if you're on Visual Studio or using a different version of the .NET Framework, adapt accordingly.

Next, let's set up a little master/detail layout to our Window that we will later populate. Replace the default <Grid> section with the following:

     <DockPanel>
        <ListBox DockPanel.Dock="Left" Width="100" />
        <Grid Height="100">
            <Grid.RowDefinitions>
                <RowDefinition />
                <RowDefinition />
                <RowDefinition />
                <RowDefinition />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="80" />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>
            <TextBlock Text="Name:" Grid.Row="0" Grid.Column="0" />
            <TextBlock Text="Surname:" Grid.Row="1" Grid.Column="0" />
            <TextBlock Text="Age:" Grid.Row="2" Grid.Column="0" />
            <TextBlock Text="Location:" Grid.Row="3" Grid.Column="0" />
            <TextBox Grid.Row="0" Grid.Column="1" />
            <TextBox Grid.Row="1" Grid.Column="1" />
            <TextBox Grid.Row="2" Grid.Column="1" />
            <TextBox Grid.Row="3" Grid.Column="1" />
        </Grid>
    </DockPanel>

This gives us the following layout:


Not the prettiest thing, but that's beside the point. On the left, we have a ListBox where we'll display a list of names. On the right, we'll display the details for the selected person.

Next, let's create our ViewModel. Right click the project, Add -> New Item... and add a new class called MainVM.cs. In your Window's codebehind (Window1.xaml.cs in SharpDevelop, or MainWindow.xaml.cs in Visual Studio), set up this ViewModel as the DataContext by adding the following line at the end of its constructor:

             this.DataContext = new MainVM();

Add a new class called Person.cs. Set it up as follows:

     public class Person
    {
        public String Name { get; set; }
        public String Surname { get; set; }
        public int Age { get; set; }
        public String Location { get; set; }

        public String NameAndSurname
        {
            get
            {
                return String.Format("{0} {1}", this.Name, this.Surname);
            }
        }
      
        public Person(String name, String surname, int age, String location)
        {
            this.Name = name;
            this.Surname = surname;
            this.Age = age;
            this.Location = location;
        }
    }

This Person class represents the information we'll be displaying in our application. We can now go back to our ViewModel and begin to set up the properties that will actually be bound in the view (Window).

At the top of MainVM.cs, add the following:

using System.Collections.Generic;
using GalaSoft.MvvmLight;

Make MainVM inherit from MVVM Light's ViewModelBase:

    public class MainVM : ViewModelBase

We can now add member variables to represent our list of people and also the one that is currently selected:

        private List<Person> persons;
        private Person selectedPerson;

Add in the corresponding properties, and don't forget the RaisePropertyChanged() to let the data binding update the user interface when the properties change. RaisePropertyChanged() is something we get from MVVM Light by inheriting from ViewModelBase - see yesterday's article if you're feeling lost.

        public List<Person> Persons
        {
            get
            {
                return this.persons;
            }
            set
            {
                this.persons = value;
                RaisePropertyChanged("Persons");
            }
        }
      
        public Person SelectedPerson
        {
            get
            {
                return this.selectedPerson;
            }
            set
            {
                this.selectedPerson = value;
                RaisePropertyChanged("SelectedPerson");
            }
        }

In MainVM's constructor, let's add some data we can work with:

            this.persons = new List<Person>()
            {
                new Person("Chuck", "Norris", 21, "The Boogeyman's closet"),
                new Person("Monica", "Lewinski", 70, "Take a guess..."),
                new Person("Steve", "Ballmer", 55, "Crying somewhere"),
                new Person("Captain", "Jack", 30, "The Black Pearl")
            };

In the Window's XAML view, we can now add data binding to the ListBox as follows:

         <ListBox DockPanel.Dock="Left" Width="100" ItemsSource="{Binding Path=Persons}"
                 DisplayMemberPath="NameAndSurname" SelectedItem="{Binding Path=SelectedPerson}" />

So our ListBox is now getting its items from the Persons property we have just defined, and for each item (of type Person it displays the NameAndSurname property. We also bind the ListBox's SelectedItem property to the SelectedPerson property we just defined. We'll use that in a minute, but first, let's hit F5 to see that we're all good so far:


See, the names now appear in the ListBox on the left. We can now bind the fields on the right to properties of the SelectedPerson:

            <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Path=SelectedPerson.Name}" />
            <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Path=SelectedPerson.Surname}" />
            <TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Path=SelectedPerson.Age}" />
            <TextBox Grid.Row="3" Grid.Column="1" Text="{Binding Path=SelectedPerson.Location}" />

Once you run the application and select one of the names on the left, you'll see the details appear on the right (I resized the window a little so that things fit a little better):


Now, this approach might work if you can afford to load all your data in memory - i.e. you don't have a lot of items, and they don't contain a lot of data. When you're dealing with larger data sets, it is probably best to take the following approach instead:

  1. Bind the ListBox to a list of objects that contain minimal information necessary for the master view to work - in this case, the name, surname, and an ID corresponding to a primary key for that record in the database.
  2. Add a SelectedPersonDetails property, in which you keep full details for the selected record.
  3. In the SelectedPerson's set modifier, implement code to fill SelectedPersonDetails with details from the database based on the selected ID.
  4. Bind the detail fields to properties of SelectedPersonDetails.

In this article, we have seen a simple way of implementing a master/detail view using the MVVM design pattern. We did this by maintaining properties representing the master view's items and selected items, and binding to them. Fields in the detail view could then be bound to properties of the selected item. In case a lot of data is involved, it is recommended to populate the list with basic data and then retrieve details lazily (on demand) as needed.

Thanks for reading; come back again! :)

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! :)