Contents
- The MVVM framework
- The model
- The view
- Das ViewModel
- Unit-Tests
- Diploma
Windows Presentation Foundation (WPF) is Microsoft's replacement for the WinForms designer. It is agiganticImprovement over what we've used for the past 20 or so years. It uses vector graphics to render the text-based XAML code you create in the designer into screens that can do anything you've ever seen on a video display.
But WPF isn't just for designing beautiful user interfaces. It also allowsUnit-Testsof your code, which is about a hundred times faster than launching the app and going to the screen you were working on. This involves using a method called Model-View-ViewModel (MVVM) where you move all the code-behind to another class called ViewModel. You can include tests in the ViewModel classes that can be run without launching the corresponding forms. Add to that the ability to easily see that the change you just made broke code elsewhere in your app, and MVVM is thatinexpensive wayto create desktop applications. And by the way, WPF apps willalwayscosts less to build and runs faster than HTML5 or any other kind of browser-based nonsense. So don't believe everything you hear.
However, MVVM is not easy. Many developers have read articles about MVVM and just decided it's not worth the trouble. Who would trade the devil you know for the devil you don't know?
The purpose of this article is to demonstrate a minimalistic approach to MVVM that you can master and use in your next application. It doesn't have all the bells and whistles that MVVM allows, but it's straightforward and you can absolutely master it today. So let's start.
About the author:I'm not an IdeaBlade employee, but when I develop I use DevForce. -Die Pinterests.
Platform:WPF
Language:C#
Download: mvvm-forms-les-pinter-610.zip
The MVVM framework
MVVM is short for Model-View-ViewModel. A view is a form; a ViewModel is a class that contains the code formerly contained in the form's code-behind file; and a model is a class that retrieves and stores your data and (most importantly) exposes it to your view (form) via WPFbinding. So the data goes in from the modelpublic propertiesin the ViewModel. Your view then uses the binding to get the data that's now in the ViewModel object and send back changes to it. The ViewModel then calls methods on the Model to store the data. The methods for retrieving and storing data reside in the ViewModel, not the earlier - er, View. They're hooked up to something called Commanding, which takes about 4 lines of code per CommandButton.
This means that you can unit test the ViewModel without launching the View, since the code that retrieves data and handles events such as button clicks is not in the View's code-behind. And that's the whole point. You don't need to launch the form to run your code. So a test bench likeIsn't it?can test and validate your code without running the program. And did I mention it's fast?
When I first heard about MVVM, I immediately sat down to write an add/edit/delete form, the heart of all database applications. I've read dozens of articles about MVVM and surprisingly none of them got me to the point of a working application. They kept telling me about the wonders of MVVM, but they didn't show me how to add or delete a record. They reminded me of the super salesman whose new bride left him the morning after their first wedding night because he sat at the foot of the bed all night telling her how great it was going to be. just keep going, okay?
So let's continue.
Select in VS 2010file,Neu,New project, and selectWPF applicationto specify a location that you can easily find later. Create a new project namedAddEditDeleteProspectsof type WPF application. The following project appears in Solution Explorer:
You probably want to use MainWindow as your splash screen and to display your main menu. You should probably change the name by renaming it, and I did this by changing the filename tofrmStartup. Note that when renaming, you must edit App.xaml and change StartUri to the new .xaml filename - this does not happen automatically. Also note that even though you change the filenames, the class name and its constructor name are still MainWindow internally. So if you also change their names internally, you must do so in both the .xaml and .xaml.cs files.
The next steps are to add a model to describe each of the tables in your database, and then add a View and ViewModel for each of the tables that require a user maintenance form. You can share a ViewModel between multiple forms, and sometimes that's appropriate. but usually there is exactly one ViewModel per View. A ViewModel is a class that interacts with the view, and a Model is a class that represents your data. You write the ViewModels, but IdeaBlade Devforce writes the models. More on that later.
But before we add our model, we need a class; it saysCommandMap.cs. It is available in the download for this article. Register (it's free) and when you reload the article, the link says "Log in or register" instead aDownload the source codeShortcut. You will find this file in the download. This little class doescommanding, the process of connecting your command button click events (and other events) to the code in the ViewModel, very simple. If I didn't include it, I'd have to give a lengthy explanation of how to command it, and that's usually the point where readers' eyes glaze over. Instead, you add three lines of code in each ViewModel, and then add one more for each command, plus the code for the method to run when the button is clicked. That is easier.
CopyCommandMap.csinto your project and then click theShow all filesicon at the top of Solution Explorer, highlight the new file, right click and select itInclude in projectfrom the resulting context menu. Look at the code if you want, but it's not necessary, except to see that the internal namespace isCommandUtilities, which you will use in your ViewModels. But first, let's add a model.
The model
I have a table called Prospects that is used in this project. In the download for the article there is a SQL script that creates a database called Tests and then adds and populates a table called Prospects. I was using SQL 2008 R2 which does constraints a bit differently. The version shown below also works with earlier versions of MS SQL.
SQL | Listing 1: Creating the test database and table: CREATE DATABASE TESTS; USAGE TESTING; CREATE TABLE [dbo].[Prospects]( INSERT IN TO Prospects VALUES ('THE','Pinter','34616 Autobahn 190','Springville','CA','93265'); |
Choosing a GUID (a Globally Unique IDentifier) is good because it makes inserting a new record key easier. In a multiuser environment, if you use an integer identity column, you must reserve the next sequential number to be reserved while the user finishes entering data for the new prospect, and then either insert or delete the record. Usually a temporary negative integer is used, but it's a nuisance no matter how you handle it. Guids are the simplest way to provide a unique key.newID()is the guid generator of SQL, and the statement shown above to create database tables uses it; but in our case we only assign one in the code as you will see below.
Before we continue, I will create a form at the end of this article to search for a specific prospect and display it in the prospect form. I have declared a public static for thisleadership fieldin App.xaml.cs to hold the user-selected prospect IDfrmFindAProspect, and then use it to retrieve the relevant prospect when I return from the search form. Add just one line in App.xaml.cs, right after the class declaration statement (the "?" means the field can beNull):
C# | public partially Class App: Application |
Note:You can also create a partial class for the Prospects entity and then add this code in the Create method and this will happen every time you create a new record. But this is an article about MVVM, not IdeaBlade DevForce, which can do more than you probably ever need or even want to know.
If you haven't already, download the demo version nowDev Force 2010ORM tool. It doesn't expire. It only supports up to ten tables, making it useful for educational purposes only. If you have an application with hundreds of spreadsheets, the value of the application justifies the high cost. But the educational version is free.
To see DevForce in action, right-click and select your project in Solution ExplorerAdd to,new itemfrom the context menu. up inBrowse installed templatesfield, typeADO. You only see a small selection.
Choose the firstADO.NET entity data model. Name it DataModel at the bottom left of the screen and clickAdd to.
You'll be asked if you want to generate the model from the database, and you certainly do.
You will be prompted to either choose a connection to the test database or create a new one. The connection string it creates is in Entity Framework format, so it might look a little strange:
Next, select your table or tables and/or views and/or stored procedures. We only have one table, prospects, so check it out and click on itOK.
If you look at the output window, you will see that it says something about the code generation. That's what the ORM tool does, and it isfast. I've seen it generate a hundred thousand lines of code in sixty seconds. You don't touch this code. If you need to add something else to one or more tables, there's an app for that; more on that later.
Your project in Solution Explorer now looks like this:
The highlighted files represent the model of the data. The .edmx file contains the map of the table(s) in the model; the .tt file references the domain model template used by the IdeaBlade ORM tool to generate the code; and the .cs file contains the generated code, part of which is shown below.
C# | Code listing 2: The model code generated by DevForce (partiallyListing): public partially Class testing entities: IbEm.EntityManager { Constructors (region collapsed) #region entity queries |
There are half a dozen classes in this generated code, but two of them are most important to us:
- AEntityManagerclass called "TestEntities" that you use to retrieve and store data; This class contains aentity querycalledperspectivesthat can be used to return toObservableCollectionfrom potential companies;
- TheOutlookclass containing a table row with prospects; For each column in the table there is a corresponding public propertyNotification of Ownership Changesbuilt-in. This is important for WPF, as we'll see in a moment.
These constructs are the framework we use to create our application.
The view
Back in frmStartup I added a line to the<windowExplanation at the beginning of the file:
C# | WindowStartupLocation="CenterScreen" |
I also added a menu with the following structure:
C# | file tables |
We must addEvent-Handlerfor the Exit and Prospects menu items. Because their names are used to generate event names, add "Name=" clauses to each. I useName=mnuExitAndName=mnuProspectsor Now select theexitMenuItem, open the properties window with F4, select theeventstab (the one with the little lightning bolt icon) and double-click theClickCase. As a resultmnuExit_ClickEvent handler method, add the following:
C# | App.Current.ShutDown(); |
But before we write the code to launch the prospects form, we need to create this form. The WPF form designer is, to put it mildly,a little differentfrom the WinForms designer. Every traditional developer I've ever worked with to create their first WPF form has asked the same question: "Why isn't the layout toolbar enabled?" That little question actually sheds a lot of light on the WPF paradigm.
In WPF, you use containers to snap and align controls. For example, if you want to stack five controls from left to right, use a stack panel. Usually you start with anetwork, which has nothing to do with datagrids or gridviews or datagridviews. In WPF, a grid is just a rectangle. You can add row and column separators and then position a control within a specific "cell" of this grid by specifying coordinates, e.g.<TextBox Grid.Row="2", Grid.Column="4" />. A StackPanel is stacked either vertically or horizontally. A UniformGrid distributes things evenly. A canvas is pretty much what you work with in WinForms, and just to give you a taste of this brave new world, it's absolutely the lamest of them all.
Containers should be nested. So if you end up with a stackpanel inside a grid inside another grid inside a stackpanel, that's probably about right; I have nested containers eight deep. Because of this, you don't have a left and top setting for each slider. However, there is a border attribute with left, top, right, and bottom spacing parameters that occasionally comes in handy. I have a few articles on XAML design and I hope to convince you that it's actually pretty easy to use, even without Blend. For now though, just try to use containers and the judicious use ofRand,upholstery,Vertical AlignmentAndHorizontal orientation, and that's usually enough.
In Solution Explorer, highlight the project name, right-click to open the context menu and selectAdd to,new item,Window. InputfrmProspectsas file name. The designer opens the new window (form) in a designer window. Add the xaml shown here:
XAML | <window <StackPanel> <grid <tag <tag <tag <tag <tag <tag </grid> <stackpanel alignment="Horizontal"Marge ="0,10,0,0"> <button name="btnFind" </StackPanel> </StackPanel> </window> |
To spice up the form's appearance, I added a few styles to mineApp.xaml, as shown below:
XAML | <Application <Application.Resources> <SolidColorBrush x:key="BG">Linen</SolidColorBrush> <Style TargetType="Network"> <Style TargetType="StackPanel"> <Style TargetType="Window"> <Style TargetType="Label"> <Style TargetType="text field"> </Anwendung.Ressourcen> </Application> |
The use of theStatic ResourcesBG, TC, and FF aren't required, but it makes it much easier to assign the same attribute (a color or a font) to multiple styles.
The form is shown below:
Now that you have a prospects form, you can go back to frmStartup.xaml, select mnuProspects, open the properties window, select the events tab and double-click the click event, and then add this code to the resulting mnuProspects_Click event handler:
C# | frmProspects fP =neufrmProspects(); fP.Show();// oder fP.ShowDialog(); |
Press F5 to run the application and launch the window.
At this point, the window contains no references to the data, and the code-behind consists of exactly one line of code:Initializing the Component;This is where the ViewModel comes into play.
Das ViewModel
For each view you create a ViewModel. The ViewModel is a class that is instantiated in the Load event of the corresponding predecessor - um View. Sorry, it's a hard habit to break. Once the ViewModel object is created, it is assigned to the View's DataContext. After that, each Binding reference is assumed to be a reference to a public property in the ViewModel. So, for example, if weOutlookproperty containing the currently selected record from the prospects table looks for a binding to FirstName in a View control in the public propertyOutlookin the ViewModel object or in a public Model object within the ViewModel. These propertiesmustbe public because mandatory usesConsiderationto find them, and they must be public, and they must be properties - that is, they must have themAccessoren- a getter and a setter.
The values stored in public properties can change during program execution. If this is the case, the ViewModel class must implement theIPropertyChangeNotificationinterface so you canRaisePropertyChanged(Name of the property)to tell the view that the data in the corresponding binding needs to be updated. To this one adds:INotifyPropertyChangedat the end of the ViewModel class declaration and add the two bold lines of code at the end of the ViewModel class as shown earlier. Then includeRaisePropertyChanged("property name")as the last line in theGetterevery property. I hid an undocumented easter egg in the app's code to demonstrate it. It's hidden in plain sight...
In order to associate ViewModel methods with events (specifically the Button Click event) in your view, you must implement theI orderInterface. That's what CommandUtilities.cs is for. It gives you an easy way to add command delegates (sorry, that's what they're called) to your code and attach them to a specific button. You can either click a button to run the appropriate command, or you can just call the method in aTestFixtureor from another method in your code. And if you write a second method that returns true if you want the button to be activated or false if you want it to be deactivated, you can pass it as a second (optional) parameter to the Commands constructor to provide that aspect of button control automate. See theExtinguishcommand for an example.
That's a lot of explanation, but if you study the code using the line number references in the following explanations, I think you'll find that most things are harder to explain than they are to do. Both binding and commanding generally only require a few lines of code.
In Solution Explorer, right-click the project name and selectAdd to,Classfrom the context menu. Give the namefrmProspects_VM, as shown:
Then add the code shown below:
C# | 01useIdeaBlade.Core; |
I definitely have something to explain, so let's get started.
- 01-07: The IdeaBlade references eliminate these prefixes before IdeaBlade classes: CommandUtilities is the NameSpace for the CommandMap class used to implement commands; and the four Windows references are for handling guids and collections;
- 10, 89–90: Used to implement a property change notification that posts changed values back to the view;
- 12: Creates an IdeaBlade EntityManager object that tracks changes and posts them back to the database; It also contains the generated query class used to return prospects in the search form defined below;
- 14: This is the collection used to store records—er, entities—returned from a query;Outlookis the generated class that defines a recorder, entity and query for this table.
- 16-17: Prospect is the object that contains the current record from the prospects table. Prospect form controls are bound to this object;
- 19: This contains a new lead that will be created and then added toperspectivesCollection;
- 21-22: This collection stores commands in a way that simplifies both command wiring and binding syntax;
- 24: This is the constructor for the ViewModel class; It runs when the object is created in the view's Load event, as we'll see in a moment.
- 26: This instantiates the EntityManager class generated by DevForce for this database;
- 28: This instantiates theperspectivescollection ofOutlookRecords - er, entities;
- 30: This instantiates the Commands collection object defined in the CommandMap class in CommandUtilities.cs;
- 31-36: This adds six delegate commands to the Commands collection; in the next listing we will see how they are used;
- 39-49: This shows a modal search form to select a single prospect to display in frmProspects. The user-selected key, which is aFührer, is stored in App.ID;
- 51-56: Not used unless you want the form to appear with a record already loaded;
- 58-60: Uses a little trick to find the view form and call the formenablerRoutine to activate/deactivate the TextBoxes and Buttons;
- 63-68: Adds an empty record to the lead collection, adds a GUID and assigns it to the lead object for display in the view;
- 70-71: Enables data entry control and disables all buttons except Save and Cancel (see Show CodeBehind);
- 73-78: deletes the selected prospect record and removes it from the prospect collection using the mgr object;
- 80-81: Returns True if the Delete button should be enabled; or False if this should not be the case; This is a built-in behavior of ICommand (seeCommandMap);
- 83-84: saves all changes and disables input controls and enables all buttons except save and cancel;
- 86-87: cancels all changes and disables input controls and enables all buttons except save and cancel;
- 89-90: Required if you implemented the INotifyPropertyChanged interface on line 10.
The strangest code in this ViewModel is this:
C# | 46 q.Execute().ForEach(prospects.Add); |
That means
"Run the query; thenAdd toeach row in the result to theperspectivesObservableCollection",
Binding the click event on buttons in a view to the corresponding ViewModel requires implementing theI orderInterface that CommandMap thanks toDelegateCommandClass in CommandUtilities.cs. Binding to data requirespublic properties, one for each column in each table, and these properties must have built-in property change notification. IdeaBlade entities do this. OurOutlookobject representing the prospect displayed on the screen, so we added it in line 17 above.
Theperspectivescollection is a public property (note { get; set; } , which implements the default backing store.) That isnecessary. Remove the { get; Sentence; } at the end and it will compile fine, but it won't work. You'll forget to add this a few times before you fully understand ObservableCollections and binding.
Probably the strangest thing about this code is the occasional use ofLambda Expressions, i.e. "x => x". (e.g. line 45). There's a reason for this syntax, thoughI do not care. Just get used to typing "x => x". (or "z => z." or whatever) if you want IntelliSense to show you your column names and you'll be fine.
Now we can go back and add the binding expressions that bind the View to the ViewModel:
XAML | <window <StackPanel> </StackPanel> <grid <tag <tag <tag <tag <tag <tag </grid> <stackpanel alignment="Horizontal"Marge ="0,10,0,0"> <button name="btnFind" </StackPanel> </StackPanel> </window> |
Adding the attributetext={binding path=first name}"causes WPF to look up its current oneData contextfor a public property named FirstName. Since the streamData contextis an instance offrmProspects_VMViewModel class and that of GridData contextis bound toOutlook, it looks like in frmProspects_VM.prospect, a public property of the typeOutlook(a single recorder - er, entity) where it finds thanks to a public property called LastNameperspectivesClass generated by IdeaBlade and declared in our ViewModel. Neat, right?
Back infrmProspects, we need a little code to bring our app to life:
C# | useSystem.Windows; namespaceAddEditDeleteProspects /* Constructor */ Private EmptybtnClose_Click(ObjectSender, RoutedEventArgs e) public Emptyenabler (booland off)// called inside the ViewModel! txtFirstName.IsEnabled = !OnOff; |
The reason the ViewModel object is available to View is because we declare and instantiate it in the constructor offrmProspects. The declaration is restricted to the class. In the constructor, the ViewModel object is assigned to the formsData context. This is why the binding code in Listing 5 works.
Enabler is a method to enable/disable TextBoxes and Buttons in the view. There are two reasons I made Enabler() a public method in the code-behind of the form: first, I don't need to test this logic in the unit tests; It only matters in the UI, and you test the UI by running the UI. Second, since Enabler() ispublic, I can call it from the ViewModel (see the ManageControls method in the ViewModel above.) The way the ViewModel code is written, if the window isn't found, the code won't run, so it will fail during unit testing is harmless if no views are active.
The last thing I need in my project is theFindAProspectForm (I mean view) referenced in line 41 of Listing 4. My example only searches for surnames starting with the letter(s) entered by the user, but of course you can make the search form as fancy as you like. The methodology is the same: filter the collection based on user-supplied criteria, and then when the user selects a prospect, store their key (which is a Guid, if you remember) in the public static ID field in app. xaml.cs . I only used LastName because it shows a way to filter the data. The bold code at the end of Listing 8 shows how LINQ should provide IntelliSense for SQL queries. Pretty smooth right?
I'll list the xaml first, then the code-behind:
XAML | <window <grid> <tag <DatenGrid <button </window> |
C# | useSystem; namespaceAddEditDeleteProspects publicFindAProspect() Private EmptybtnShow_Click(ObjectSender, RoutedEventArgs e) Private EmptybtnSelect_Click(ObjectSender, RoutedEventArgs e) Private EmptybtnCancel_Click(ObjectSender, RoutedEventArgs e) } |
If you've never used LINQ, the code in btnShow_Click will be a bit confusing.perspectivesis actually oneentity query, meaning it is the basis of a SELECT statement. LINQ exposes SQL with IntelliSense; Each time you enter a point, the PEMs (Properties, Events, and Methods) available at that point are displayed in a drop-down list. C# ignores whitespace, including spaces and carriage returns, so you can move to the next line before you type the period for readability (or if you're a publisher and have more vertical than horizontal space; welcome to my world). Finally, you run the query, add all returned rows to the prospects collection, and assign them to the datagridArticleSource.
The Where, OrderBy, ThenBy clauses, and any others you might want to use, need a starting point for the object whose PEMs you are exposing and x=>x. means something like "an x so that the x's..." Just type it in and the available PEMs will be delivered. Looks weird but works. After a while, you'll use it without even thinking about it. Which is better than trying to streamline such a strange syntax. But Microsoft decides how we do our jobs, and they're always right, even when a mutant like this is the result. But it's the best of all possible syntaxes, isn't it?
<rant>No, it's not. Straight from the head, this would be better:
C# | var query = mgr. Prospects P |
It's just a thought, gods in Redmond...</rant>
btnSelect_Click zeigt wasApp.IDis for; It provides a place to store the selected Guid until we close the FindAProspect modal form and return to the code that started it, which then uses the value stored in App.ID to find the selected prospects record - er, entity - get and load into it the prospect object in frmProspects_VM that exposes the columns as public properties (with property change notification, thanks to DevForce), which is thebinding path = (column name)Attributes in the view to find and display the data. And since Binding in WPF is TwoWay by default, changes to the form are posted back to the publicOutlookObject in the ViewModel to be stored by the mgr object. got it
You can also easily use a ViewModel for the search form, but this is not required. MVVM is used to enable unit testing; it is not a religion. You can also add test routines in code-behind files; You just have to write separate [test] methods so they don't require human input. For example, I added a [Test] to FindAProspect CodeBehind in the download. It is described in the Unit Tests section below.
Speaking of unit testing, wasn't that the original goal? So let's do it.
Unit-Tests
Unit testing is one of the reasons for using MVVM instead of just putting all event handling in the code-behind. So it had better be pretty simple and pretty smooth. It's both.
I use nUnit because it has stood the test of time and has tons of features. You can download it for free from nUnit.org. Add after installationnUnit.exeto your start menu (ornUnit-x86.exeif you are using a 64-bit processor). You will use it a lot. Then add your project references a reference to nUnit.Framework.dll located in your C:\Program Files(x86)\nUnit folder and build the project.
Adding a test couldn't be simpler (well it could, but that would require a change in the way Microsoft implemented usings and that would add a bunch of other complications. I miss FoxPro...<g> ) Add usingnUnit.FrameworkOnlyunderthe namespace declaration in the frmProspects_VM.cs file, and then add the following:
C# | [TestFixture, Description("Unit Tests")] |
If you addwith NUnit.Frameworkbefore the namespace declaration you must use NUnit.Framework.Description("...") because a Description attribute is also associatedSystem.ComponentModel, and VS 2010 doesn't know which one you are referring to. I had never used usings except at the beginning of a code file, but it's never too late to learn something new.
Next change theGetFirstRecordmethod look like this:
C# | [Test, Description("Test on at least one record")] |
The [RequiresSTA] attribute is required. I had never seen this documented in IdeaBlade code, but believe me it won't work without it.
I also added a test in the code-behind for the search form just to show that it works. I have last names that start with "P" and "S" but not with "A". So I'm going to test whether a corresponding data set comes back, which is totally coolValues-Parameter:
C# | [Test, Description("Returns at least one surname beginning with 'S' or 'P'; matching 'A' fails")] |
Now start nUnit (or nUnit-x86) and use File/Open to point to your executable, either in the bin/Debug or bin/Release folder and click the GO button. Once you have loaded the test data from the SQL script in SQL Listing 1, you will see the result shown:
Unit tests can be as simple or as robust as you want them to be. Some developers write the unit tests before creating the forms. That's a bit hard core for me, but whatever floats your boat.
Diploma
WPF is a huge leap forward in desktop application design, and MVVM is a great way to take full advantage of its powerful binding capabilities. Hopefully this article has given you the confidence to dive in and try it out at one of your tables. Email me if you can't get something to work. I've been answering all my emails at Les@Pinter.com for 25 years; no reason to stop now.
Bye
MVVM Light mit DevForce (Silverlight)" previous
Next "Code Sample: MVVM Made Easy (WPF)