Search code examples
wpfmvvmlinq-to-entitiesmvvm-light

Data Validation with MVVM-Light WPF and Linq to Entity Framework


I think I have read every article google returns when I search wpf mvvm-light data validation and I dont know which way to go. I am aware of josh smith, Karl Shifflett's, and MVVM LIGHT's own demo techniques for data validation. What I see is that most validation requires me to fully "re-abstract" my model in my view model. Meaning that I have to create a property in my viewmodel for each property of my model that I want to validate (and in some cases convert all these into string values for binding/validation). This seems like a lot or redundancy when all I want to do is mark most fields as required.

I am using LINQ to entity framework(with self tracking) for my model classes which come from a SQL server DB. As a result I would prefer to keep my business data validation/rules within my viewmodels. I write a simple service interface to get the data from the model and pass it to my viewmodel.

Most of the examples I can find are from as far back as 2008 (ie josh smith). Are these techniques still valid or are there more up to date best practices for mvvm data validation with .NET 4.5 etc.

So I am asking:

1) What methods do you suggest I use 2) What methods work best in a LINQ to EF with MVVM-Light Environment. 3) EDIT: I want to provide feedback to user as they enter data, not just when they submit form

thanks


Solution

  • I eventually ended up using the following. I changed my model to use LINQ to self tracking entities (see this article for info about STE http://msdn.microsoft.com/en-us/library/vstudio/ff407090%28v=vs.100%29.aspx).

    LINQ to STE creates an OnPropertyChanged event that implements the iNotifyPropertyChanged interface.

    I just created a public partial class for the matching model object (linq entity generated code) I wanted and added an event handler for the OnPropertyChanged event. I then used the IDataErrorInfo interface to validate and throw errors as I needed. This allows me to validate the fields as they change which gets reflected to the user. This also allows you to perform more advanced validation logic that may need to requery the database (i.e. to look for if a username is already used etc.) or throw a dialog box

    Also, having the data validation in the model allows me to still have validation if i perform direct "batch" operations that bypass the UI.

    I then used an HasErrors and HasChanges property and used them to create a Boolean value that gets attached to the relay commands, disabling the crud command buttons if errors are present.

    I will post some simple code to outline what I just described, comment if you want more detail.

    Here is the Entity Framework extension of the model class:

     Imports System.ComponentModel
    
    
    Partial Public Class client
    
        Implements IDataErrorInfo
    
    #Region "Properties / Declarations"
    
        'Collection / error description
        Private m_validationErrors As New Dictionary(Of String, String)
        Private _HasChanges As Boolean = False
    
        ''Marks object as dirty, requires saving
        Public Property HasChanges() As Boolean
            Get
                Return _HasChanges
            End Get
            Set(value As Boolean)
                If Not Equals(_HasChanges, value) Then
                    _HasChanges = value
                    OnPropertyChanged("HasChanges")
                End If
            End Set
        End Property
    
        'Extends the class with a property that determines
        'if the instance has validation errors
        Public ReadOnly Property HasErrors() As Boolean
            Get
                Return m_validationErrors.Count > 0
            End Get
        End Property
    
    #End Region
    
    #Region "Base Error Objects"
        'Returns an error message
        'In this case it is a general message, which is
        'returned if the list contains elements of errors
        Public ReadOnly Property [Error] As String Implements System.ComponentModel.IDataErrorInfo.Error
            Get
                If m_validationErrors.Count > 0 Then
                    Return "Client data is invalid"
                Else
                    Return Nothing
                End If
            End Get
        End Property
    
        Default Public ReadOnly Property Item(ByVal columnName As String) As String Implements System.ComponentModel.IDataErrorInfo.Item
            Get
                If m_validationErrors.ContainsKey(columnName) Then
                    Return m_validationErrors(columnName).ToString
                Else
                    Return Nothing
                End If
            End Get
        End Property
    
    #End Region
    
    #Region "Base Error Methods"
    
        'Adds an error to the collection, if not already present
        'with the same key
        Private Sub AddError(ByVal columnName As String, ByVal msg As String)
            If Not m_validationErrors.ContainsKey(columnName) Then
                m_validationErrors.Add(columnName, msg)
            End If
        End Sub
    
        'Removes an error from the collection, if present
        Private Sub RemoveError(ByVal columnName As String)
            If m_validationErrors.ContainsKey(columnName) Then
                m_validationErrors.Remove(columnName)
            End If
        End Sub
    
    #End Region
    
        Public Sub New()
    
            Me.HasChanges = False
        End Sub
    
    #Region "Data Validation Methods"
    
        ''handles event and calls function that does the actual validation so that it can be called explicitly for batch processes
        Private Sub ValidateProperty(ByVal sender As Object, ByVal e As PropertyChangedEventArgs) Handles Me.PropertyChanged
            If e.PropertyName = "HasChanges" Then
                Exit Sub
            End If
            IsPropertyValid(e.PropertyName)
            HasChanges = True
        End Sub
    
        Public Function IsPropertyValid(sProperty As String) As Boolean
            Select Case sProperty
                ''add validation by column name here
                Case "chrLast"
                    If Me.chrLast.Length < 4 Then
                        Me.AddError("chrLast", "The last name is too short")
                        Return True
                    Else
                        Me.RemoveError("chrLast")
                        Return False
                    End If
                Case Else
                    Return False
    
            End Select
    
        End Function
    
    #End Region
    
    End Class
    

    then in the view model I included the following code to bind thecommand and evaluate whether or not it can be executed.

     Public ReadOnly Property SaveCommand() As RelayCommand
            Get
                If _SaveCommand Is Nothing Then
                    _SaveCommand = New RelayCommand(AddressOf SaveExecute, AddressOf CanSaveExecute)
                End If
                Return _SaveCommand
            End Get
        End Property
    
        Private Function CanSaveExecute() As Boolean
            Try
                If Selection.HasErrors = False And Selection.HasChanges = True Then
                    Return True
                Else
                    Return False
                End If
            Catch ex As Exception
                Return False
            End Try
    
        End Function
    
        Private Sub SaveExecute()
            ''this is my LINQ to Self Tracking Entities DataContext
            FTC_Context.SaveChanges()
        End Sub
    

    the following is how I bound my button (has custom styling in WPF)

     <Button Content="" Height="40" Style="{DynamicResource ButtonAdd}" Command="{Binding SaveCommand}" Width="40" Cursor="Hand" ToolTip="Save Changes" Margin="0,0,10,10"/>
    

    so, when there are no validation errors and the current client record "isDirty" the save button automatically becomes enabled, and disabled if any of those two conditions fail. This way I now have a simple way of validating any type of column/data I want for the entity, and I can provide user feedback as they enter data in the form, and only enable CRUD command buttons once all my "conditions" have been met.

    This was quite a battle to figure out.