Share via


WPF/Entity Framework Core primer (C#)

Introduction

 Intended audience is hobbyist, windows forms developers interested in working with WPF (Windows Presentation Foundation) projects with Entity Framework Core for data operations. To get the most from this article a prerequisite is to read the following Microsoft tutorial (no need to work with the code, just read) which provides a base to work from.

Purist will have beginners start with MVVM (Model-View-ViewModel) pattern which will not be used. For some what's presented will be all that is needed for creating applications while other developers will want to move on to implementing the MVVM pattern for more complex applications. Refer to WPF: MVVM Step by Step for a MVVM primer.

Part 2 WPF/Entity Framework Core simple data validation (C#)

SQL-Server database

The database is a fictitious human resource department focusing on the employee table while the other tables can be used to experiment with while learning to work with Entity Framework Core and WPF. Note the article project has only reversed engineered the employee table, to reverse engineer the database use the following Visual Studio extension EF Core Power Tools.

Before working with the article project run the database script to create the database and populate tables.

User interface

Focus will be on the employee table presenting first and last name of each employee in a DataGrid with a button on each row which opens a child window displaying details for the current row.

Entity Framework Core

For this article the latest release of Entity Framework Core is 3.1.4 

DbContext

Once the desire table (in this case) employee has been reverse engineered a DbContext class named HRContext.cs is created which handles the connection string to the database which by default is hard coded while in this example the connection string resides in app.config file here which allows moving from a development database to test then production databases. In OnModelCreating method of the context class code has been provided to describe the database tables, names, columns, if a column is required, field length for strings and table relationships.

Tip many times a developer does not have control of column names in a table e.g. first_name and a more desirable name would be FirstName, in OnModelCreating a column can be mapped to a different name as shown here.

entity.Property(e => e.FirstName)
    .HasColumnName("first_name")
    .HasMaxLength(20)
    .IsUnicode(false);

Caution foreign key names are used to indicate a relationship e.g.

entity.HasKey(e => e.EmployeeId)
    .HasName("PK__employee__C52E0BA894F7579A");

if the relationship name changes in the database and not in code this results in a run time exception so make sure if the key name changes update the code and note this must be done manually. To assist, the following script will provide key names.
 

Model

Generally there are several tables used that are reversed engineered, in this article only one table has been reverse engineered which is the employee table. Each table when a class is created there is a property for each column in the tables along with navigation properties which reference related tables.

The default when reverse engineered

using System;
using System.Collections.Generic;
 
namespace WpfApp1.Models
{
    public class  Employees
    {
        public Employees()
        {
            InverseManager = new  HashSet<Models.Employees>();
        }
 
        public string  Type { get; set; }
        public int  EmployeeId { get; set; }
        public string  FirstName { get; set; }
        public string  LastName { get; set; }
        public string  Email { get; set; }
 
        public string  PhoneNumber { get; set; }
        public DateTime HireDate { get; set; }
        public int  JobId { get; set; }
        public decimal  Salary { get; set; }
        public int? ManagerId { get; set; }
        public int? DepartmentId { get; set; }
        public virtual  Models.Employees Manager { get; set; }
        public virtual  ICollection<Models.Employees> InverseManager { get; set; }
    }
}

When working with the WPF user interface and changes are made the underlying collection, ObservableCollection has change notification but not the user interface. For the user interface to be notified the Employee class needs to implement INotifyPropertyChanged Interface.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using WpfApp1.Annotations;
 
namespace WpfApp1.Models
{
    public partial  class Employees : INotifyPropertyChanged
    {
        private int  _employeeId;
        private string  _firstName;
        private string  _lastName;
        private string  _email;
        private string  _phoneNumber;
        private DateTime _hireDate;
        private int  _jobId;
        private decimal  _salary;
        private int? _managerId;
        private int? _departmentId;
 
        public Employees()
        {
            InverseManager = new  HashSet<Employees>();
        }
 
        public int  EmployeeId
        {
            get => _employeeId;
            set
            {
                if (value == _employeeId) return;
                _employeeId = value;
                OnPropertyChanged();
            }
        }
 
        public string  FirstName
        {
            get => _firstName;
            set
            {
                if (value == _firstName) return;
                _firstName = value;
                OnPropertyChanged();
            }
        }
 
        public string  LastName
        {
            get => _lastName;
            set
            {
                if (value == _lastName) return;
                _lastName = value;
                OnPropertyChanged();
            }
        }
 
        public string  Email
        {
            get => _email;
            set
            {
                if (value == _email) return;
                _email = value;
                OnPropertyChanged();
            }
        }
 
        public string  PhoneNumber
        {
            get => _phoneNumber;
            set
            {
                if (value == _phoneNumber) return;
                _phoneNumber = value;
                OnPropertyChanged();
            }
        }
 
        public DateTime HireDate
        {
            get => _hireDate;
            set
            {
                if (value.Equals(_hireDate)) return;
                _hireDate = value;
                OnPropertyChanged();
            }
        }
 
        public int  JobId
        {
            get => _jobId;
            set
            {
                if (value == _jobId) return;
                _jobId = value;
                OnPropertyChanged();
            }
        }
 
        public decimal  Salary
        {
            get => _salary;
            set
            {
                if (value == _salary) return;
                _salary = value;
                OnPropertyChanged();
            }
        }
 
        public int? ManagerId
        {
            get => _managerId;
            set
            {
                if (value == _managerId) return;
                _managerId = value;
                OnPropertyChanged();
            }
        }
 
        public int? DepartmentId
        {
            get => _departmentId;
            set
            {
                if (value == _departmentId) return;
                _departmentId = value;
                OnPropertyChanged();
            }
        }
 
        public virtual  Employees Manager { get; set; }
        public virtual  ICollection<Employees> InverseManager { get; set; }
        public event  PropertyChangedEventHandler PropertyChanged;
 
        [NotifyPropertyChangedInvocator]
        protected virtual  void OnPropertyChanged([CallerMemberName]  string  propertyName = null)
        {
            PropertyChanged?.Invoke(this, new  PropertyChangedEventArgs(propertyName));
        }
    }
}

Main window

To provide the ability to update any changes to the database a private instance of the DbContext is added to the window.

private readonly  HRContext Context;
public MainWindow()
{
    InitializeComponent();
    Context = new  HRContext();
}

Actual loading of data from the database is done by initializing a ObservableCollection<Employees> followed by assigning the collection to the ItemSource of the DataGrid.

var employeeCollection = new  ObservableCollection<Employees>(Context.Employees.AsQueryable());
 
EmployeeGrid.ItemsSource = employeeCollection;

To view information for a row the following XAML defines a button as the first column in the DataGrid. Code for the button click event which cast the DataContext of the button to the current row employee followed by passing the employee to a child window. Note Owner is set which allows the child window to be centered on the current window.

private void  ViewCurrentEmployee(object sender, RoutedEventArgs e)
{
    var employee = (Employees) (sender as  Button)?.DataContext;
 
    var window = new  DetailsWindow(employee) {Owner = this};
    window.ShowDialog();
     
}

XAML in child window to set the window location which will not work unless Owner is set as per the last code block.

WindowStartupLocation="CenterOwner"

Code behind for the child form. In the constructor the employee passed is set to a private window variable then on window loaded properties of the employee are set to controls.

Important
Since many of the properties can be null a null check is done via ? (Null-Conditional operators). So if an employee has no manager without ? a null exception would be thrown at runtime.

using System.Windows;
using WpfApp1.Models;
 
namespace WpfApp1
{
    /// <summary>
    /// Interaction logic for DetailsWindow.xaml
    /// </summary>
    public partial  class DetailsWindow : Window
    {
        private Employees _employee;
 
        public DetailsWindow()
        {
            InitializeComponent();
        }
 
        public DetailsWindow(Employees employee)
        {
            InitializeComponent();
            _employee = employee;
        }
 
        private void  Window_Loaded(object  sender, RoutedEventArgs e)
        {
            var manager = _employee?.Manager;
 
            if (_employee != null)
            {
                FirstNameBox.Text = _employee.FirstName;
                LastNameBox.Text = _employee.LastName;
            }
 
            ManagerNameBox.Text = $"{manager?.FirstName} {manager?.LastName}";
 
        }
    }
}

DataGrid Filtering

Filtering data is a common request and with a TextBox, subscribing to TextChanged event then in code using a CollectionViewSource default view pointing to the ItemSource of the Datagrid filtering is fast and easy to implement.

In the following code the search text for filtering on last name of an employee is obtained, if there is no text the filter is removed, otherwise a case insensitive filter is applied to the last name property.

private void  LastNameSearchTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
    var tb = (TextBox)sender;
    var lastNameFilter = tb.Text;
    var cvs = CollectionViewSource.GetDefaultView(EmployeeGrid.ItemsSource);
 
    // nothing entered to search so remove an existing filter
    if (string.IsNullOrWhiteSpace(lastNameFilter))
    {
        cvs.Filter = null;
    }
    else
    {
        // do the filter
        cvs.Filter = item => 
            item is  Employees employees && (employees.LastName.StartsWith(lastNameFilter, 
               StringComparison.InvariantCultureIgnoreCase));
    }
}

Finding data

To find data, in this code sample cast the DataGrid.ItemSource to ObservableCollection<Employees> following by using LINQ or Lambda to search.

/*
 * Find employee by last name, if found
 * select and scroll into view in the DataGrid
 */
var employee = (employeeCollection)
    .FirstOrDefault(emp => emp.LastName == "Russell");
 
if (employee == null) return;
 
EmployeeGrid.SelectedItem = employee;
EmployeeGrid.ScrollIntoView(employee);

The last two lines will select the found item followed by ensuring the found item is in the viewport.

Adding new records

Although there is no button or window in this code sample to add a new record there is a mocked up method to add a new record.

/// <summary>
/// Example for adding a new employee without going thru the hassle of
/// rigging up user interface as there are plenty needed to collect
/// required fields.
///
/// Note no manager assigned, this means in the view button click we
/// need to do a null check.
/// </summary>
private void  AddHardCodedEmployee()
{
    // create new employee
    var employee = new  Employees()
    {
        FirstName = "Jim",
        LastName = "Lewis",
        Email = "jlewis@comcast.net",
        HireDate = new  DateTime(2012, 3, 14),
        JobId = 4,
        Salary = 100000,
        DepartmentId = 9
    };
 
    // add and set state for change tracker
    Context.Entry(employee).State = EntityState.Added;
 
    // add employee to the grid
    ((ObservableCollection<Employees>)EmployeeGrid.ItemsSource).Add(employee);
 
}

Important
Any properties that are marked as required in the database and the DbContext must be set, For the EF Core ChangeTracker to be aware of the record being added the State must be set e.g. Context.Entry(employee).State = EntityState.Added;.

Deleting records

In this code sample pressing the DEL key would remove a row in the DataGrid without prompting "are you sure" so to ensure this does not happen by accident the following event will prompt to confirm the delete operation or cancel the operation.

/// <summary>
/// Prompt to remove the current row in the DataGrid
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void  Grid_PreviewCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
    var grid = (DataGrid)sender;
 
    if (e.Command != DataGrid.DeleteCommand) return;
 
    if (grid.SelectedItem is Employees employee)
    {
        var employeeName = $"{employee.FirstName} {employee.LastName}";
        if (!Question($"Delete {employeeName}", "Confirm Delete"))
        {
            e.Handled = true;
        }
    }
}

The event is subscribed in XAML

<DataGrid 
    x:Name="EmployeeGrid" CommandManager.PreviewCanExecute="Grid_PreviewCanExecute"

Note the method Question, this is from the following class which keeps code clean to provide a yes/no prompt with the default button set to "no" so if the user happens to simply press enter no action is taken.

Monitoring user interface changes

The following event shows from a developer perspective ongoing changes and deletes which would not be used in a production application, solely for learning.

private void  EmployeeCollection_CollectionChanged(
    object sender, NotifyCollectionChangedEventArgs e)
{
    if (e.OldItems == null) return;
    Console.WriteLine(e.Action);
    if (e.Action != NotifyCollectionChangedAction.Remove)  return;
 
    var employee = (Employees)e.OldItems[0];
    InformationDialog($"Index: {e.OldStartingIndex} - " + 
                      $"{employee.FirstName} {employee.LastName}", "Just removed");
 
}

Monitoring database operations

Entity Framework Core provides the ability to see SQL generated by Entity Framework Core via a NuGet package. To see how this can be implemented see the following article's section on logging.

Save changes

The following button click event first checks to see if there are changes using the ChangeTracker for delete, modified or added records. If there are changes ask the user to save or cancel.

  • Try-catch statement is used to capture any errors including validation errors e.g. a require property is not set. Although not covered in this article check out the following project included in the source code which provides method to determine if there are issues prior to saving data to the database. This will be covered in a future article and linked back here.
  • Issues with a disconnected database server
private void  SaveChangesButton_Click(object sender, RoutedEventArgs e)
{
    
    try
    {
        // we only have to deal with deletes and modified items
        var affectedCount = Context.ChangeTracker.Entries().Count(entry => 
            entry.State == EntityState.Deleted || 
            entry.State == EntityState.Modified || 
            entry.State == EntityState.Added);
 
        if (affectedCount > 0)
        {
            if (Question("Save changes?"))
            {
                Context.SaveChanges();
            }
        }
        else
        {
            InformationDialog("Nothing to save.");
        }
    }
    catch (Exception ex)
    {
        ExceptionDialog("Something went wrong", "Ooops", ex);
    }
}

XAML

No matter what level of knowledge the reader has there is always something to learn new or different ways to construct a user interface. When working with WPF and not sure how to do something, always look for a solution away from code.  

Coming from Windows forms there is a richer environment with WPF. For instance, closing a child window the first though for most developers would be to create a button, subscribe to the click event and find code to close the window. While asking "can this be done with XAML?" The answer is yes.

<Button
    Content="Close"
    Grid.Row="3" Grid.Column="2"
    HorizontalAlignment="Left"
    Margin="3,8,20,0"
    VerticalAlignment="Top"
    Width="75"
    IsCancel="True"
    Height="20"/>

See IsCancel.

Explore application styles rather than repeat styles for controls, for instance in this article the DataGrid style is in app.xaml.

When first creating WPF applications expect to spend a good deal of time perfecting the user interface and to speed this up forget about trial and error, instead read documentation on working with XAML

Summary

Information presented provides a decent starting point for creating simple data centric solutions with Windows Presentation Foundation using Entity Framework Core and SQL-Server database. Consider this a first step into a rich user interface environment that on the data end requires less code than in conventional window form projects.

External resources

Data binding overview in WPF
Microsoft code samples
Building an Enterprise App with WPF, MVVM, and Entity Framework Code First

See also

Easy MVVM examples (in extreme detail)
WPF ListBox data template/styling
WPF: get all controls of a specific type using C#
MVVMExtraLite Companion guide
Displaying and Editing Many-to-Many Relational Data in a DataGrid
WPF/C# Setting visibility of controls based off another control
TechNet WPF portal

Source code

Clone or download source from the following GitHub repository. Once opened in Visual Studio run the data script, in solution explorer run restore NuGet packages.