Tuesday, October 17, 2017

Passing MVVM Model classes to and from WCF services

MVVM is a great paradigm and it makes sense to use the models to pass data to and from the WCF Services (or whatever backend you use). This blog examines some of the things that you need to be aware of.

It is written in VB targeting framework 4.5 and later.

Start by creating the WPF project. It will contain an Application, a MainWindow, and eventually a reference to the WCF endpoint.


We will create a data model that is a subclass of a base class. For example, you might have a Transaction base class and JournalEntry, PurchaseOrder, and PayVoucher subclasses. In our project we will have a Thing class and a Person class. We will also add a method and an event to the Person class.

We have to put the model classes in the WCF project. If we put them in the WPF project we will end up with circular references which are not allowed.

At this point we need to add the WCF project. Right-click on the Solution in the Solution Explorer, click Add, then select New Project...


The new project will be a WCF Service Application and will be named "WCFPerson".


In the Solution Explorer rename IService1.vb to IPerson.vb and Service1.svc to Person.svc. In Person.svc.vb rename Class Service1 to PersonService. You can leave the old names in place if you prefer, but they are not a useful names.

Now let's define our Thing and Person classes. Let's start by adding a new class to WCFPerson called Thing. This class will contain a single string property called Name (because everything has a name) that is decorated as a DataMember and will implement NotifyPropertyChanged. All derived classes will be able to use this property and interface. The DataContract and DataMember decorations mean we can transfer objects of this class between WPF and WCF. The implementation of INotifyPropertyChanged means we can bind controls to objects of this class.

Imports System.ComponentModel
Imports System.Runtime.CompilerServices

<DataContract()>

Public Class Thing
    Implements INotifyPropertyChanged

    Private _Name As String


    <DataMember()>

    Public Property Name As String
        Get
            Return _Name
        End Get
        Set(value As String)
            If _Name <> value Then
                _Name = value
                NotifyPropertyChanged("Name")
            End If
        End Set
    End Property

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Public Function SetProperty(Of T)(storage As T, value As T, <CallerMemberName> Optional PropertyName As String = "") As Boolean
        If Object.Equals(storage, value) Then Return False
        storage = value
        NotifyPropertyChanged(PropertyName)
        Return True
    End Function
    Public Sub NotifyPropertyChanged(PropertyName As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(PropertyName))
    End Sub
End Class

Note that the base class MUST be decorated with DataContract even if it doesn't have any members that participate in the contract. This is because any class used in a DataContract can only derive from a class that is decorated with DataContract.

Let's add the sub class. Create a new class called Person in WCFPerson.

Public Class Person
    Inherits Thing

    Public Event AgeChanged(OldValue As String, NewValue As String)


    Private _Age As Integer


    <DataMember()>

    Public Property Age As Integer
        Get
            Return _Age
        End Get
        Set(value As Integer)
            If _Age <> value Then
                RaiseEvent AgeChanged(_Age, value)
                _Age = value
                NotifyPropertyChanged("Age")
            End If
        End Set
    End Property

    Public Sub IncrementAge()

        Age += 1
    End Sub
End Class

This inherits Thing. Because Thing already implements INotifyPropertyChanged, the Person class does not need to explicitly. The Person class does not need DataContract because the Thing class already has it, but it still needs to mark any members that need to be part of the data contract with DataMember decorations. The Person class adds an Age property to Thing's Name property, an AgeChanged event, and an IncrementAge method.

Now it is time to define our interface and code in WCFPerson. Replace the contents of IPerson.vb with this. It defines a single method called GetPerson that returns a new Person object.

<ServiceContract()>
Public Interface IPerson

    <OperationContract()>

    Function GetPerson() As Person

End Interface


We will implement our interface in Person.svc.vb like so...

Public Class WCFPerson
    Implements IPerson

    Public Function GetPerson() As Person Implements IPerson.GetPerson
        Return New Person() With {.Name = "Bob", .Age = 40}
    End Function
End Class

Now we are ready to build the WCF project. Right-click on WCFPerson and select build. Once you have a clean build we will add a service reference to the ModelAcrossTheWire project by right-clicking on References and selected Add Service Reference.



Now click [Discover], set the namespace to PersonServiceReference, and click [OK].


Now we have a service reference we can complete the WPF part of this solution. We're going to create a public property of type Person, bind to it, then populate it from the WCF service as MainWindow is constructed.

Replace MainWindow.xaml with this. Note I'm using a poor man's version of MVVM for simplicity.

<Window x:Class="MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ClassAcrossTheWire"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Grid>
        <TextBlock>
            <TextBlock.Text>
                <MultiBinding StringFormat="{}{0} is {1}">
                    <Binding Path="Person.Name"/>
                    <Binding Path="Person.Age"/>
                </MultiBinding>
            </TextBlock.Text>
        </TextBlock>
    </Grid>
</Window>

Now replace MainWindow.xaml.vb with this. Even though the Person class implements INotifyPropertyChanged, the Person property itself still needs it's own implementation. The MainWindow will populate Person and then increment their age through the IncrementAge method. This causes the AgeChanged event to be raised which is caught and displays a MessageBox.

Imports System.ComponentModel
Imports WCFPerson

Class MainWindow

    Implements INotifyPropertyChanged

    Private WithEvents _Person As Person


    Public Property Person As Person

        Get
            Return _Person
        End Get
        Set(value As Person)
            If Not value.Equals(_Person) Then
                _Person = value
                NotifyPropertyChanged("Person")
            End If
        End Set
    End Property

    Public Sub New()

        InitializeComponent()
        Person = GetPerson()
        Person.IncrementAge()
    End Sub

    Public Function GetPerson() As Person

        Dim svc As PersonServiceReference.PersonClient = Nothing
        Try
            svc = New PersonServiceReference.PersonClient()
            Return svc.GetPerson()
        Catch ex As Exception
            Throw
        Finally
            If svc IsNot Nothing Then svc.Close()
        End Try
    End Function

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Public Sub NotifyPropertyChanged(PropertyName As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(PropertyName))
    End Sub

    Private Sub AgeChanged(OldValue As String, NewValue As String) Handles _Person.AgeChanged

        MessageBox.Show("Age Changed from " & OldValue & " to " & NewValue)
    End Sub
End Class

So the take away from this project is that we are able to bind to the same model we used to retrieve the data. That model can include properties, methods, and events that do not interfere with the use of the model as a DataContract.

Happy birthday Bob!


No comments:

Post a Comment