If you're just getting into Windows Presentation Foundation, you've probably heard that the recommended paradigm, or pattern, or whatever you like to call it, is Model-View-ViewModel. It's actually pretty cool. Unfortunately, the guys who like to show their superiority by slinging the acronym around seem to be more enamored with the terminology than they are with making it simple. Making it simple is my mission in life. So here goes.
About the author: I'm not an IdeaBlade employee, but when I develop, I use DevForce. - Les Pinter.
Platform: WPF
Language: C#
Download: mvvm-made-simple-les-pinter-607.zip
I'm not crazy about our industry's proclivity for providing arcane acronyms for programming constructs. MVVM should be called TFCBR - Table-Form-CodeBehindReplacement. Look at the solution in Figure 1, below.
MainWindow.xaml is the form. MainWindow.xaml.cs is the CodeBehind for the form. MainWindow_VM.cs is the ViewModel - the CodeBehind replacement. IBModel.NorthWindModel.IB.Designer.cs contains the Model code with the table classes generated by the IdeaBlade ORM tool, which you can download here. After you decide that you can't live without it, which if I'm not mistaken should be in about twenty minutes. It's about a grand. If you've been struggling with Entity Framework, that will seem like a bargain.
The first M in MVVM is for Model - a class that represents queries, tables and records. You'll hear them called entitles. They're queries, tables and records, all rolled into one. I suppose that deserves a new name, but I think that queries would have sufficed.
NorthWindModel.IB.Designer.cs contains the Model code that both loads and stores data for any and all of your tables. I only asked for one model to be generated, but I've generated several hundred before, and it takes seconds. You don't touch the generated code. It's complete right out of the box. But you don't write the model code; you download IdeaBlade DevForce 2010 and install it, then take about 30 seconds creating models for all of the tables in your database, and you don't really ever have to look at them again. The Model part is truly simple.
The V is for View. A view is a form - a WPF form. WPF forms are called Windows, and consist of text files containing Extended Application Markup Language - XAML (pronounced zammel). There are a few other types, but mainly you'll use Windows. You can use the WPF Designer that's built into Visual Studio 2010, or you can code the XAML by hand, or you can use Microsoft Expression. I haven't yet mastered Expression, but although I'm sure it's a great tool, I'm able to do everything I need without it. XAML is intimidating at first, but it kind of grows on you. It's truly amazing what you can do with it visually. You'll love it...eventually.
Like WinForms forms, there's a separate CodeBehind file. But in WPF, it contains almost no code. The VM in MVVM stands for ViewModel. That's a class that hooks the data to the View, and contains all of the code for the View. The ViewModel is where most of the code that would ordinarily go in the window's CodeBehind file goes. And since it's not directly dependent upon keystrokes or mouseclicks in the user interface, you can install test routines there that can be run without typing in the window. This is probably the most important benefit of the MVVM pattern. So you can expect a future article on unit testing with MVVM real soon now.
So what links them together? The magic that does it is called binding. In the form's CodeBehind, you add one line of code after InitializeComponent() to tell it to load its associated ViewModel (usually named the same as the associated View (window) with VM at the end) when the form initializes. The DataContext is where WPF looks for public properties to bind to.
Here's the entire CodeBehind for MainWindow:
C# | Listing 1: MainWindow.xaml.cs using System.Windows; namespace IB_MVVM { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); Loaded += delegate { DataContext = new MainWindow_VM(); }; } } } |
The View uses Binding statements to link various components of the User Interface (UI) to the ViewModel. It does so using constructs like this:
C# | Text="{Binding Path=CompanyName}" |
This tells the UI to look in the ViewModel for a public property called CompanyName. Not companyName. Not cOmPaNyName. The geniuses at Microsoft decided to make this case sensitive. Don't get me started.
The tie-in with IdeaBlade is that DevForce (their product name, different from the company name) builds models with a public property for each column name, so when you add your Model into your ViewModel, the Binding mechanism in WPF roots around until it finds what it's supposed to bind to. Doesn't matter where you put it. Once you've added your Model object to your ViewModel, if the Model contains a public CompanyName property, WPF binding will find it, bind to it, display it, and let you change it so that the changes can be saved. Amazing, isn't it?
I've built a minimalist example to demonstrate how MVVM works. My form (Fig. 2, below) uses a ComboBox to display the letters of the alphabet. As the user uses the down-arrow to move from one letter to the next, the ViewModel loads records in which CompanyName starts with the value of the Letter parameter into custs collection in the ViewModel.
A file called App.xaml in the project contains <style> sections that sets the attributes (horizontal/vertical orientation, color, fonts, etc) of the labels, textboxes, grids and other UI elements in your windows. The more you put there, the less you have to write in your Windows' XAML. So I always add styles for every control and at a minimum, set the HorizontalAlignment and VerticalAlignment properties.
The little app shown in Figs. 1 and 2 demonstrates how MVVM works. I hope you'll find it useful and simple enough to understand and replicate. As usual, I use the Customers table in the Northwind database (actually, the IdeaBlade Northwind Sample Database, which installs with DevForce). The app lets users select a letter from a DropDownList and see customers whose names start with the selected letter:
Fig. 2 - The sample application form
XAML | Listing 2: MainWindow.xaml <Window x:Class="IB_MVVM.MainWindow" Title=" IdeaBlade DevForce MVVM Demo " Height="450" Width="500" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" WindowStartupLocation="CenterScreen"> <Expander Header="Hide/Show Grid" IsExpanded="True"> <Grid Width="475"> <Label Style="{StaticResource ScreenTitle}" <!-- The ScreenTitle style is stored in app.xaml --> Content=" IdeaBlade Devforce MVVM Demo " Margin="5,3,0,0" /> <DataGrid ItemsSource="{Binding custs}" BorderBrush="AliceBlue" BorderThickness="3" Margin="3,61,3,0" Width="470" AutoGenerateColumns="False" AlternatingRowBackground="LightBlue"> <DataGrid.Columns> <DataGridTextColumn Binding="{Binding Path=CompanyName}" Header="Company" Width="220" /> <DataGridTextColumn Binding="{Binding Path=ContactName}" Header="Contact" Width="220" /> </DataGrid.Columns> </DataGrid> <Label Content="Show companies with names starting with " Margin="6,38" /> <ComboBox Name="cmbFirstLetter" Text="{Binding Path=firstLetter}" Margin="240,36" Width="36"> <ComboBoxItem Content="A" /> <ComboBoxItem Content="B" /> ... <ComboBoxItem Content="Z" /> </ComboBox> </Grid> </Expander> </Window> |
The x:Class declaration tells the window where the codebehind is stored (Project IB_MVVM, file MainWindow.xaml.cs). The two namespace declarations are used by WPF and are always included. WindowStartupLocation does what it looks like it does. The expander is just there because it looks so darned cute.
The boldfaced lines in the XAML are the story. They tell the UI engine to bind to data stored in an object called custs for the grid. Binding="{Binding Path=CompanyName}" and Binding="{Binding Path=ContactName}" tell the grid columns which data columns (actually, which properties) to get from the table data stored in the custs object. The custs object is a Model, as in Model-View-ViewModel, and it's built as an IdeaBlade LINQ query - more on that later. Note that XAML is case-sensitive; "CompanyName" and "ContactName" are exactly how the two column names are spelled in the Customers table from whence these data come. "companyname" won't get you a thing.
The App.xaml file tells the project which is the startup screen - the one containing your menu, if you have one. In our case, there's only one window, so that's the startup window.
But the real work of the App.xaml file is to style all of the controls in all of the windows in your application. This one is very simple, but some contain thousands of lines of style code. You can also point to an external "Resource Dictionary", and your users can change it at execution time. That's all it takes to implement themes in WPF.
XAML | Listing 3: App.xaml <Application x:Class="IB_MVVM.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="MainWindow.xaml"> <Application.Resources> <Style TargetType="Grid"> <Setter Property="Background" Value="LightYellow" /> </Style> <Style x:Key="ScreenTitle" TargetType="{x:Type Label}"> <Setter Property="Background" Value="Red"/> <Setter Property="Foreground" Value="White"/> <Setter Property="FontFamily" Value="Verdana"/> <Setter Property="FontSize" Value="16"/> <Setter Property="HorizontalAlignment" Value="Left"/> <Setter Property="VerticalAlignment" Value="Top"/> <Setter Property="Effect"> <Setter.Value> <DropShadowEffect BlurRadius="4" Direction="-55" Color="Black" ShadowDepth="4"/> </Setter.Value> </Setter> </Style> <Style TargetType="Label"> <Setter Property="HorizontalAlignment" Value="Left" /> <Setter Property="VerticalAlignment" Value="Top" /> <Setter Property="Height" Value="18" /> <Setter Property="Padding" Value="1" /> <Setter Property="Width" Value="auto" /> </Style> <Style TargetType="Button"> <Setter Property="HorizontalAlignment" Value="Left" /> <Setter Property="VerticalAlignment" Value="Top" /> <Setter Property="Height" Value="auto" /> <Setter Property="Width" Value="auto" /> </Style> <Style TargetType="ComboBox"> <Setter Property="HorizontalAlignment" Value="Left" /> <Setter Property="VerticalAlignment" Value="Top" /> <Setter Property="Height" Value="auto" /> <Setter Property="Width" Value="auto" /> </Style> </Application.Resources> </Application> |
As you can see, ScreenTitle is a visual style defined in App.xaml. Style="{StaticResource ScreenTitle}" tells the label to get its styling info (font size, color, etc.) from App.xaml. When you download the code, start by tweaking this style. It's way cool!
But the real smoke and mirrors is the Text property binding on the ComboBox in MainWindow.xaml. firstLetter is a public property of the ViewModel. Speaking of which, here's the ViewModel for this window (there's one per window):
XAML | Listing 4: MainWindow_VM.cs: The ViewModel for MainForm.xaml using IdeaBlade.Core; using IdeaBlade.EntityModel; using System.Collections.ObjectModel; using IBModel; namespace IB_MVVM { class MainWindow_VM { private NorthwindIBEntities _mgr = new NorthwindIBEntities(); public ObservableCollection<customer> custs { get; private set; } public string FirstLetter = ""; public string firstLetter { get { return FirstLetter; } set { if (FirstLetter != value) { FirstLetter = value; getByFirstLetter(FirstLetter); } } } public MainWindow_VM() { custs = new ObservableCollection<customer>(); firstLetter= "A"; // Load the names that start with A } public void getByFirstLetter(string letter) { custs.Clear(); var query = _mgr.customers.Where(x => x.CompanyName.StartsWith(letter)); //This displays the grid, then loads it when the data is returned: query.ExecuteAsync(op => op.Results.ForEach(custs.Add)); //If you prefer to load the grid before displaying it, do this: //query.Execute().ForEach(custs.Add); } } } |
What's a _mgr? If you open the NorthwindIBModel.IB.Designer.cs file, you'll see a class called NorthwindIBEntities. That's the IdeaBlade EntityManager class that contains the generated table queries. The only reason that I can think of for even looking at the generated code is to retrieve the name of this class, so that you can add the _mgr object declaration to your ViewModel code. I'm sure that after you've done it a few hundred times, you'll know what the generated name will be. But until them, here's were you can find it. Note that _mgr is private because it's only used in the ViewModel code; custs is public because the XAML in the View has to be able to see it.
In C#, the difference between public properties and public fields is that properties have a getter that runs when you retrieve the value of the associated private field, and a setter that runs when you assign the associated private field a value. In WPF with MVVM, that's one of the ways that you can make things happen in your UI. When you assign firstLetter a value in your code, or set it interactively by selecting a new value from the ComboBox, the setter code fires. In this case, the setter code includes getByFirstLetter(Letter), which loads the custs collection, which is bound to the datagrid via the ItemsSource binding. Any questions?
Okay, that's a good question. IdeaBlade is Entity-Framework on steroids. In case you've been busy, Entity-Framework is how Microsoft wants us to get our data. If you add an ADO Entity Model to your project, a wizard will ask you which table(s) you want to use, and will then write thousands of lines of code that expose the columns of your tables as public properties so that WPF can bind things to them. It also builds LINQ (Language INdependent Query) queries, which are what you use to get your data. You execute a query and bind the result to the DataContext (or ItemsSource) of your WPF controls, and you're done. If you install IdeaBlade DevForce, The IdeaBlade ADO Entity Model will generate IdeaBlade-compliant code, and you'll get a whole lot more bang for your buck. It also does a lot of the work for you - caching data, change notification, and more.
Add a new class project called IBModel to your solution, and immediately delete the Class1.cs file that it adds by default. Add a new item, an ADO Entity Model, to the class project. Following the steps in the resulting wizard, add the appropriate connection string, and then pick the Customers table from the list of Northwind tables. Name the saved code file NorthwindIBModel. Compile the project and add a reference to it to your UI project. Then, drag the app.config file from the Model project to the UI project, so that the program will know where to get the connection string to your data. (You'll also need to add references to three IdeaBlade dlls: IdeaBlade.Core, IdeaBlade.EntityModel, and IdeaBlade.Validation.) That should have taken all of 90 seconds. If it didn't, get a faster computer.
But Houston, we've got a a problem. LINQ is completely different from what you're used to. So get ready to look for working examples and copy them until this begins to make sense. The more you work with LINQ queries, the more sense they'll make. I've used an asynchronous query, but I also included an Execute() query, commented out.
I need to mention something about the code that's a little confusing. The ObservableCollection declaration and the constructor both refer to "customers". The syntax ObservableCollection<ClassName> uses the generics syntax introduced in an earlier version of the .NET framework.) But the actual query refers to _mgr.customer. What's up with that?
The answer can be found in the generated Model code. Customer is a record structure. Customers is a LINQ query that also contains a collection of Customer records; custs is an instance of the Customer LINQ query. That's why you see the use of custs.Add() to add customer records to the query result.
But what are those using statements at the top of the code? The first line permits leaving out the qualifier prefixes on objects that come from those classes, which are added as references when you add IBModel to the UI project's references. The next using is to simplify the reference to the ObservableCollection interface; and the third line references our Model, so that we don't have to prefix the Customers reference with the IBModel project name.
By declaring custs as a public property, we make it available to the View (the Window). By declaring it as an ObservableCollection, we get some additional functionality that helps in binding. All that remains is to create the custs object in the constructor, and assign a value to the public property firstLetter. That triggers the setter code, and we're off and running.
getByFirstLetter is called when we assign a value to firstLetter, whether in code (as in the constructor, above) or in the DropDownList, where we had the innocent-looking XAML Text="{Binding Path=firstLetter}". That's all it takes to run the query when the user either selects a letter from the DropDownComboBox or simply enters a letter using the keyboard.
getByFirstLetter(string Letter) calls a LINQ query that loads data for the grid into the custs ObservableCollection. It first clears custs, and then constructs and executes the query. I've used an asynchronous query, which ordinarily requires a delegate as a target for the requisite callback. The lambda expression that's passed as a parameter in the example saves you the bother of writing a separate delegate function, but it does the same thing.
You could also use query.Execute(). Either one loads custs, which contains the public properties that expose the data in the columns of the Customer records. Note that both Execute and ExecuteAsync require the addition of a ForEach(custs.Add()) loop. That tells the query to take the records - er, entities from the query and add them to the resultset.
Anyway, notice that you don't have to get the data from each column and and assign it to a control. The "Path=" attribute takes care of it. The binding does the finding. (I just made that up.) That, it turns out, is the least of what binding in WPF does.
There's a lot more to MVVM. For example, there's commanding, the process that you use to tie keystrokes (like hotkeys, function keys and menu commands like Edit, New and Delete) to specific methods in your ViewModel. That's the subject of an upcoming article in this series. The implementation is elegant - which is what I've come to expect from WPF. You're going to like it - I guarantee it.
See 'ya.
Les