Click here to Skip to main content
15,867,330 members
Articles / Desktop Programming / WPF
Article

Using WPF MVVM for Database Access

Rate me:
Please Sign up or sign in to vote.
4.97/5 (31 votes)
2 Nov 2010CPOL13 min read 210.9K   27.2K   113   22
An example of a WPF MVVM utility accessing a simple legacy database.

Introduction

I like to keep examples of useful code handy as they can be invaluable when beginning a new project. One of the examples I keep is a tiny database containing a few tables and Stored Procedures, with a corresponding WPF utility to access the database. The example was very standard, straight WPF calling DB procedures via SQLCommands. Over time, I realized the example had to be updated to reflect current coding practices. Specifically, I wanted the WPF utility to use the MVVM pattern and employ LINQ to SQL to access the database. The database itself was not changed. This article describes the resulting code, what needed to be added, and surprises along the way.

Recognizing that few will be interested in code that requires installing a database, I have tried to make it more appealing by demonstrating some unrelated issues. This includes gel buttons and seamless repeating background tiles. This is my initial attempt at MVVM. Let me know how I did.

Getting Started

The database xstoredb must be installed. Download either the source or exes. In the top directory is a file named CreateStoreDb.sql. Open this file with an editor and copy the contents to the clipboard. Run up SQL Server Management Studio and open a new query window. Paste the code on the clipboard into the query window and click the Execute button. This will create an unpopulated database named xstoredb. The database is a simple one, just two small tables and four Sstored Procedures. However, it provides features seen on the production databases.

After installing the xstoredb database, one should know the connection string necessary to connect with it. This connection string must be made available to the included ProductMvvm utility for it to successfully connect to xstoredb. Providing a correct connection string can be frustrating. Before you begin, you should have a good idea what the string should look like. The connection string is made available to ProductMvvm in the application configuration file. Open ProductMvvm.exe.config, you will see the connection string resides in the following XAML...

XML
<connectionStrings>
  <add name="ProductMvvm.Properties.Settings.xstoredbConnectionString"
    connectionString="Data Source=DOUG-PC;Initial Catalog=xstoredb;Integrated Security=True"
    providerName="System.Data.SqlClient" />
</connectionStrings>

Modify the value of the connectionString attribute on line 10 to specify the connection string required to access the xstoredb database you installed. The value shown above specifies my home PC (DOUG-PC) using Windows Integrated Security. Only a single line in the config file requires modification. Once a valid connection string is provided, the ProductMvvm utility will run.

Run up ProductMvvm.exe to test the connection string. As the database is initially empty, no products will be shown. The database connection may be checked by clicking the DB Refresh button. The status field should display OK, indicating the utility was able to access the database. Fill in the product fields and use the Add button to create products in the database. A snapshot of the display after adding a few products is shown below. The operator is about to delete the third product selected in the blue product selector screen at the top of the display.

ProductMvvm display

About the Display

XML
<Window x:Class="ProductMvvm.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:vw="clr-namespace:ProductMvvm.Views"       
    Title="ProductMvvm" Height="550" Width="370" MinHeight="550"
    WindowStartupLocation="CenterScreen" Loaded="Window_Loaded">
    <Grid>
        <Grid.Background>
            <ImageBrush ImageSource="LightBrushedx.jpg" TileMode="Tile" 
                            ViewportUnits="Absolute" Viewport="0,0,200,200">
            </ImageBrush>
        </Grid.Background>

        <Grid.RowDefinitions>
            <RowDefinition Height="3*"></RowDefinition>
            <RowDefinition Height="Auto"></RowDefinition>
            <RowDefinition Height="7*"></RowDefinition>
        </Grid.RowDefinitions>
        <vw:ProductSelectionView Grid.Row="0"/>
        <GridSplitter Grid.Row="1" HorizontalAlignment="Stretch" 
            VerticalAlignment="Bottom" ResizeBehavior="PreviousAndNext"
            Height="5"/>
        <vw:ProductDisplay Grid.Row="2"/>
    </Grid>
</Window>

An examination of the Window1.xaml file reveals a simple grid with three rows. The rows contain a ProductSelectionView, a GridSplitter, and the ProductDisplayView. Note the definition of the vw namespace for the views and its use to reference the classes for these views. Two views are used to keep the code reasonably simple but allow illustration of multiple views interacting with each other in a decoupled fashion. The Window_Loaded event is just a placeholder, it is not used in the code. Also note the use of LightBrushedx.jpg as a repeating background tile for the grid. Examine the display to verify the tile is indeed seamless. The reader will have to forgive my artistry. I was trying for a brushed metal look. However, the tile is interesting enough to suggest potential.

ProductSelectionView is a listbox displaying the model names of all products. In the database, the ModelName of a product is defined to be Unique. The operator selects a single product by clicking on a model name in ProductSelectionView. A control click may be used to deselect a product. The presence or absence of a selection determines what database operations are permitted. A selection is required to Update or Delete a product. A selection is not permitted when a new product is being Added. The image above shows a product being deleted. Note: there is a selected product. The Add button is grayed out because a selection is active.

ProductDisplayView is the more complicated view. It includes an uncomfortable amount of functionality in order to limit this example to two views. ProductDisplayView contains command buttons to access the database, text fields to display a selected product, and a status field showing database status or error messages. The text fields displaying a product can present what looks like WPF's error checking. Actually, WPF error checking facilities were not employed. Home grown error checking was used to minimize operator distraction. Product text fields are not checked as the operator modifies them. Error checking is performed at the last possible moment when the operator clicks a button. The specific button clicked determines which, if any fields, are error checked. For example, a Delete only requires that a product is selected. Field error checking is not performed for Deletes as the contents of the fields is immaterial for the success of the requested operation.

The command buttons are briefly described below...

  • DB Refresh - Re-initiates the utility. The database is accessed again to get all the current products. Use this button to confirm that product modifications are being entered in the database correctly.
  • Clear - A convenience function. Any selected product is deselected, and all product text fields are cleared.
  • Update - The selected product is updated on the database with the contents of the product text fields.
  • Delete - The selected product is deleted from the database.
  • Add - A new product is created on the database using the contents of the product text fields.

The Views

MVVM views typically avoid the use of named elements, bind to public data fields in the code, and make use of commanding. The two views in the ProductMvvm utility are standard MVVM. The XAML for ProductSelectionView is shown below to illustrate:

XML
<UserControl x:Class="ProductMvvm.Views.ProductSelectionView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:vw="clr-namespace:ProductMvvm.Views"        
    xmlns:vm="clr-namespace:ProductMvvm.ViewModels"
    xmlns:foundation="clr-namespace:MvvmFoundation.Wpf">
        
    <UserControl.DataContext>
        <vm:ProductSelectionModel />
    </UserControl.DataContext>

    <Grid>
        <ListBox Margin="10" Background="LightSkyBlue"
            ItemsSource="{Binding DataItems}" 
            DisplayMemberPath="ModelName"
            SelectedItem="{Binding SelectedProduct}"
            foundation:CommandBehavior.RoutedEventName="SelectionChanged"
            foundation:CommandBehavior.TheCommandToRun="{Binding Path=ListBoxCommand}">
        </ListBox>
    </Grid>
</UserControl>

Both views are UserControls. Note the definition and usage of the vm namespace to set the DataContext for the UserControl. The associated ViewModel is being instantiated by the XAML. XAML instantiated ViewModels require parameterless constructors. The expected databindings are present. Something strange is going on with the command for the SelectedProduct and the foundation namespace. More on this later. The XAML for ProductDisplayView is very similar. Due to its length, it is not reproduced here. However, it employs the same principles.

About the Foundation

An MVVM application contends with a set of common problems that are frequently encountered. This is especially true when multiple Views and their associated ViewModels are present. Luckily, there are all kinds of MVVM Foundations or Frameworks written by gifted people which elegantly solve MVVM problems. The Foundations range from simple to all inclusive complex solutions that Do Everything But Eat (a.k.a. Debbie). My approach has been to put together a Foundation by selectively taking only what I need from simple, free solutions available on the web. None of the code in the Foundation directory was written by me. Josh Smith or Sacha Barber are the authors. I may have gone overboard with the Foundation. One could hand craft alternate solutions. But using the Ffoundation produced simple standardized code. I was also able to do more with just XAML. The problems solved include...

  1. Messenger.cs - Supports decoupled messaging between ViewModels in a Publish Subscribe fashion. The message sender does not know which, if any, ViewModels are receiving the message.
  2. RelayCommand.cs - Uses Delegates to relay a commands functionality to another object. Also supports CanExecute.
  3. CommandBehavior.cs - Used to attach commands to any WPF element.

ViewModel Communication

ProductSelectionModel maintains a viewable collection of the products in the database. The model is self populating. It accesses the database to retrieve all the products. ProductDisplayModel does the remainder of database manipulation. It accesses the database as products are Added, Deleted, or Updated. The two ViewModels communicate with one another as the operator makes changes. For example, when the operator cancels a selection in the ProductSelection view, ProductDisplayModel is informed of the cancellation so it knows it can not perform a Delete or Update until another product is selected. Similarly, when a product is Deleted in ProductDisplayModel, ProductSelectionModel is informed so it can remove the deleted product from its viewable collection. Inter ViewModel communication is done through a Messenger object. Although it may seem like complicated behavior, we can get a quick summary of which messages a View Model is interested in by looking at the View Model's constructor. The constructor registers for the types of messages it wants to receive as well as the action to be performed upon receipt.

C#
public ProductDisplayModel()
{
    Messenger messenger = App.Messenger;
    messenger.Register("ProductSelectionChanged", 
                 (Action<Product>)(param => ProcessProduct(param)));
    messenger.Register("SetStatus", (Action<String>)(param => stat.Status = param));
} //ctor

public ProductSelectionModel()
{
    dataItems = new MyObservableCollection<product />();
    DataItems = App.StoreDB.GetProducts();      //populate yourself
    listBoxCommand = new RelayCommand(() => SelectionHasChanged());
    App.Messenger.Register("ProductCleared", (Action)(() => SelectedProduct=null));
    App.Messenger.Register("GetProducts",
        (Action)(() => DataItems = App.StoreDB.GetProducts()));
    App.Messenger.Register("UpdateProduct",
        (Action<Product>)(param => UpdateProduct(param)));
    App.Messenger.Register("DeleteProduct", (Action)(() => DeleteProduct()));
    App.Messenger.Register("AddProduct", (Action<Product>)(param => AddProduct(param)));
}

ViewModel Messaging

Modeling a Product

Several classes are used to represent a product retrieved from the database. A retrieved product consists of information from the Product and Category tables. The information is a mixture of string and numeric data. The XAML Views bind to fields in the Product class defined within the ViewModels folder in the file Product.cs. On error, XAML bindings fail silently. For example, when the operator enters string or illegal numeric data into a XAML element bound to a numeric data field, the binding fails without indicating an error. This may leave an unexpected value in the data field. A program usually encounters this kind of error far from the binding when some operation mysteriously fails.

To avoid XAML binding failures, all bound fields in the Product class are defined as strings. String fields accept whatever the operator types. Any possible XAML binding error is eliminated. The data fields will always contain what the operator entered. The use of string fields sidesteps any binding problems but introduces another. String fields cannot be used to accept or provide numeric database data. The Product class is great for binding data to views, but unacceptable for SQL operations. The SqlProduct class is used to satisfy SQL. This class has numeric fields as well as methods to convert to/from a Product object. The SqlProduct class is used by the single module responsible for accessing the database.

Two additional classes, LinqProduct and LinqCategory, are created by Visual Studio when the database is mapped. These classes are used within LINQ queries. Their use is transitory as they are immediately converted to Product objects. As one would be hard pressed to find these classes in the code, the code fragment below from GetProducts() is provided to illustrate their use.

C#
MyObservableCollection<Product> products = new MyObservableCollection<Product>();
try
{
    LinqDataContext dc = new LinqDataContext();
    var query = from q in dc.LinqProducts
        select new SqlProduct{              //convert to SqlProduct objects
            ProductId = q.ProductID, ModelNumber = q.ModelNumber,
            ModelName=q.ModelName, UnitCost = (decimal)q.UnitCost,
            Description = q.Description, CategoryName = q.LinqCategory.CategoryName
        };
    foreach (SqlProduct sp in query)        //convert SqlProduct to Product
        products.Add(sp.SqlProduct2Product());
} //try

The use of multiple classes to represent a product may seem unexpected or overly complex. However, one can make their usage short-lived and isolated. The Product class is used all throughout the code. In comparison, SqlProduct, LinqProduct, and LinqCategory usage is confined to the StoreDb.cs file where they are used as a bridge to form Product objects.

Error Checking Product Displays

ProductDisplayViewModel checks data fields before invoking SQL to access the database. Any errors in fields required by the specific database operation are outlined in red, and an error message is displayed in the Status field. The image below illustrates an error display. The operator is attempting to update a product with an illegal Unit Cost and a missing Category field. The operator must fix the highlighted fields in error and resubmit the Update.

Error Checking

I chose not to use WPF's error checking facility as it was not a good fit with the way the application works. The class ProductDisplayModelStatus is responsible for error checking, and is part of the ViewModel for product display. This class checks specific product fields depending on which operation is invoked. Homegrown error checking adds complexity to both the XAML and ViewModel. However, it gives one the freedom to do any type of error checking.

The ProductDisplay View defines a BorderBrush around each TextBox used to display a Product value. After an operation is invoked, ProductDisplayModelStatus examines Product values and determines what brush should be used to outline the TextBox. This means ProductDisplay must provide bindable data fields for each TextBox's BorderBrush value. Fragments from the ProductDisplay View and ProductDisplayModelStatus class are shown below for just the UnitCost field.

XML
<TextBox Margin="5" Grid.Row="2" Grid.Column="1"  
   BorderBrush="{Binding Path=Stat.UnitCostBrush}" BorderThickness="1"
   Text="{Binding Path=DisplayedProduct.UnitCost, UpdateSourceTrigger=PropertyChanged}">
</TextBox>
C#
public class ProductDisplayModelStatus : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, e);
    }

    public SolidColorBrush UnitCostBrush
    {
        get { return unitCostBrush; }
        set { unitCostBrush = value; 
              OnPropertyChanged(new PropertyChangedEventArgs("UnitCostBrush")); }
    }

The Model

Database Tables

The xStoreDb database consists of the two tables shown above and four Stored Procedures. All access to the legacy database was done by invoking the Stored Procedures via the Store.cs file. Originally, this was done using SqlCommands. Before the SqlCommand logic could be replaced with LINQ to SQL, the database had to be mapped. I let Visual Studio do the mapping. Adding a LINQ to Sql classes item to the project opens up the O/R Designer. Dragging both database tables from the Server Explorer to the left pane of the O/R Designer mapped the tables. Subsequently, the Stored Procedures were dragged to the designer's right pane. If you try to drag a Stored Procedure to a table in the left pane, you will get a schema error. This completed mapping the database for LINQ to SQL.

Deciding how to employ LINQ to SQL is a personnel decision. While I had no qualms about using LINQ to retrieve database information, I preferred the safety of Stored Procedures to modify the database. Accordingly, I replaced the GetProducts() Stored Procedure with a LINQ query which has already been shown. The remaining Stored Procedures were invoked with LINQ rather than SqlCommands. Using LINQ to invoke Stored Procedures is surprisingly easy, and simplifies the code, making it more readable. All the LINQ modifications are in the file StoreDb.cs. I left the legacy SqlCommand code commented out to show the difference with the LINQ replacement. Both mechanisms for invoking DeleteProduct() are shown below:

C#
//LEGACY SQLCOMMAND code to invoke Stored Procedure
public bool DeleteProduct(int productId)
{
    hasError = false;
    SqlConnection con = new SqlConnection(conString);
    SqlCommand cmd = new SqlCommand("DeleteProduct", con);
    cmd.CommandType = CommandType.StoredProcedure;
    cmd.Parameters.Add("@ProductId", SqlDbType.Int, 4);
    cmd.Parameters["@ProductId"].Value = productId;
    try
    {
        con.Open();
        int rows = cmd.ExecuteNonQuery();
    }
    catch (Exception ex)
    {
        errorMessage = "DELETE error, " + ex.Message;
        hasError = true;
    }
    finally
    {
        con.Close();
    }
    return !hasError;
}// DeleteProduct()


//LINQ TO SQL replacement code
public bool DeleteProduct(int productId)
{
    hasError = false;
    try
    {
        LinqDataContext dc = new LinqDataContext();
        dc.DeleteProduct(productId);
    }
    catch (Exception ex)
    {
        errorMessage = "Delete error, " + ex.Message;
        hasError = true;
    }
    return !hasError;
}// DeleteProduct()

Gel Buttons

The appearance of the gel buttons is defined in the file GelButtonResourceDictionary.xaml. It embeds a style for button elements in a ResourceDictionary. This simplifies reuse of the style in other projects. Just drop the file into a project and merge it. In this project, the gel resource dictionary is merged into application resources in the App.xaml file. This defines the appearance of every button in the application. Merging a resource dictionary file only takes a few lines of XAML. App.xaml is reproduced below:

XML
<Application x:Class="ProductMvvm.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="Window1.xaml">
    <Application.Resources">
            <ResourceDictionary">
                <ResourceDictionary.MergedDictionaries">
                    <ResourceDictionary Source="GelButtonResourceDictionary.xaml"/">
                </ResourceDictionary.MergedDictionaries">
            </ResourceDictionary">
    </Application.Resources>
</Application>

Seamless Tiles

The seamless tile represents a random pattern that goes on forever without repeating itself. Examples would be brushed metal, rust, marble, or wood grain. The tile was created in Photoshop. There are many tutorials on the Web showing techniques to make repeating tiles. Basically, you come up with a random pattern tile you like. The tile is not repeatable as the edges will not line up. The tile is then turned inside out to make it repeat. Now all the edges blend into one another, but there are visible seams in the interior of the tile. The tile is completed by employing Photoshop to heal any interior seams. So far, I have not seen any other WPF program do this.

The End

I use this project as an example. It's a starting point to creating better things. I have tried to tie together a lot of things I saw on the Web into one package. Putting it together, I was surprised at how often I had to go to multiple sites on the Web to get the MVVM working. Hopefully, it will help someone out. If you see something wrong or know of a way to improve the code, please let me know.

History

  • 02-Nov-2010: Initial release.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
PraiseVery Very Helpfull Pin
Vamsi Chakradhar4-Nov-19 17:40
Vamsi Chakradhar4-Nov-19 17:40 
GeneralMy vote of 2 Pin
korkis25-Sep-17 17:10
korkis25-Sep-17 17:10 
QuestionGreat! Helpful! Pin
moscowautist29-Jul-16 10:27
moscowautist29-Jul-16 10:27 
QuestionDatabase Access Pin
Member 1226599622-Apr-16 6:58
Member 1226599622-Apr-16 6:58 
QuestionCannot understand this error message Pin
Member 1228077723-Feb-16 6:39
Member 1228077723-Feb-16 6:39 
GeneralUsing WPF MVVM for Database Access Pin
Member 1180994321-Jul-15 0:16
Member 1180994321-Jul-15 0:16 
GeneralRe: Using WPF MVVM for Database Access Pin
Doug Wyrembek21-Jul-15 5:25
Doug Wyrembek21-Jul-15 5:25 
QuestionGreat! Pin
Tony Horray16-Jan-15 9:01
Tony Horray16-Jan-15 9:01 
QuestionWPF MVVM VALIDATION Pin
Arun Karuvatta23-Oct-14 22:50
professionalArun Karuvatta23-Oct-14 22:50 
QuestionVery useful article Pin
berezov30-Oct-13 15:14
berezov30-Oct-13 15:14 
GeneralInformative Pin
hanumanc20-Jul-13 10:19
professionalhanumanc20-Jul-13 10:19 
QuestionSqlite Pin
SDavisworth26-May-13 12:56
SDavisworth26-May-13 12:56 
SuggestionButton styles are confusing Pin
leiyang-ge12-May-13 16:20
leiyang-ge12-May-13 16:20 
GeneralRe: Button styles are confusing Pin
berezov30-Oct-13 15:12
berezov30-Oct-13 15:12 
The user should be more friendly, not the interface
Generalarticle time saving Pin
Dacorie Smith29-Mar-13 8:04
Dacorie Smith29-Mar-13 8:04 
QuestionIs this strictly in MVVM pattern? Pin
Aritra Bhowmick5-Mar-13 23:28
Aritra Bhowmick5-Mar-13 23:28 
AnswerRe: Is this strictly in MVVM pattern? Pin
Sharron Denice9-Mar-14 3:54
professionalSharron Denice9-Mar-14 3:54 
AnswerRe: Is this strictly in MVVM pattern? Pin
Sharron Denice9-Mar-14 14:24
professionalSharron Denice9-Mar-14 14:24 
GeneralMy vote of 5 Pin
catchbobbie28-Jan-13 1:00
catchbobbie28-Jan-13 1:00 
GeneralMy vote of 5 Pin
Vincent Beek7-Sep-11 9:38
Vincent Beek7-Sep-11 9:38 
GeneralMy vote of 5 Pin
raquibalam23-Dec-10 19:55
raquibalam23-Dec-10 19:55 
GeneralHi.. Pin
guyet0529-Nov-10 10:26
guyet0529-Nov-10 10:26 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.