Gigi Labs

Please follow Gigi Labs for the latest articles.

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 { getset; }
        public String Surname { getset; }
        public int Age { getset; }
        public String Location { getset; }

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

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.