Click here to Skip to main content
15,861,125 members
Articles / Web Development / IIS

WCF by Example - Chapter XIII - Business Domain - Parent and Child Relationships

Rate me:
Please Sign up or sign in to vote.
4.87/5 (13 votes)
7 Jan 2011CPOL9 min read 61K   24   16
Development of parent/child relationships across application layers - also AutoMapper, a WPF custom converter, and WPF shutdown.
PreviousNext
Chapter XIIChapter XIV

The series

WCF by Example is a series of articles that describe how to design and develop a WPF client using WCF for communication and NHibernate for persistence purposes. The series introduction describes the scope of the articles and discusses the architect solution at a high level. The source code for the series can be found at CodePlex.

Chapter overview

At this point in the series, we have covered the most important infrastructure components; however, our business domain consists of a single entity which doesn't help to explain how to resolve some common scenarios when designing parent-child relationships across different application layers. In this chapter, we are introducing a new entity to the model so we can describe how the above mentioned cases might be resolved.

At the end of this chapter, in the appendix section, we also discuss the following topics:

  • How to execute the application in Visual Studio
  • Couple WPF aspects added in this chapter: Custom WPF Converter and Explicit Application Shutdown
  • AutoMapper

Model entity overview

Up to this point, our model consisted of a single class: Customer. We are adding a new entity named Address, this is a simple entity that contains customer address details:

C#
public class Address :EntityBase
{
01  protected Address(){}

02  public virtual Customer Customer { get; private set; }
    public virtual string Street { get; private set; }
    public virtual string City { get; private set; }
    public virtual string PostCode { get; private set; }
    public virtual string Country { get; private set; }

03  public static Address Create(IRepositoryLocator locator, AddressDto operation)
    {
        var customer = locator.GetById<Customer>(operation.CustomerId);
        var instance = new Address
        {
            ...
        };

        locator.Save(instance);
        return instance;
    }

    public virtual void Update(IRepositoryLocator locator, AddressDto operation)
    {
        UpdateValidate(locator, operation);
        ...
        locator.Update(this);
    }

    private void UpdateValidate(IRepositoryLocator locator, AddressDto operation)
    {
        return;
    }
}

The entity has a reference to the Customer reference (line 02) so we will have a one-to-many relationship. As we did with the Customer class, we hide the constructor (line 01) so the Create static method (line 03) needs to be invoked when a new instance is required.

The Customer class needs some re-factoring to accommodate for the new Address class; couple important points are that the Customer class will be responsible for the creation and deletion of Address instances and that the collection of addresses is not exposed directly to ensure the collection is well managed; see also how ISet needs to be used because of NHibernate:

Image 3

C#
01 public virtual ReadOnlyCollection<Address> Addresses()
{
    if (AddressSet == null) return null;
    return new ReadOnlyCollection<Address>(AddressSet.ToArray());
}

02 public virtual Address AddAddress(IRepositoryLocator locator, 
                  AddressDto operation)
{
    AddAddressValidate(locator, operation);
    var address = Address.Create(locator, operation);
    AddressSet.Add(address);
    return address;
}

03 public virtual void DeleteAddress(IRepositoryLocator locator, long addressId)
{
    DeleteAddressValidate(locator, addressId);
    var address = AddressSet.Single(a => a.Id.Equals(addressId));
    AddressSet.Remove(address);
    locator.Remove(address);
}

Instead, the collection is exposed by cloning the collection into a ReadOnlyCollection (line 01). If a new address needs to be added to the customer, the AddAddress method must be used (line 02); the same applies when an address is to be removed (line 03).

As a result of the above changes, the domain model is as follows:

Image 4

The following changes need to be added to the NHibernate mapping file:

XML
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" 
                   assembly="eDirectory.Domain" 
                   namespace="eDirectory.Domain.Entities">
  
  <class name="Customer" table="Customer">
    ...

01  <set name ="AddressSet" fetch="subselect">
      <key column="Customer_ID" 
        foreign-key="FK_Customer_Address" 
        not-null="true" />
      <one-to-many class="Address"/>
    </set>
  </class>

  <class name="Address" table="Address">
    <id name="Id" type="Int64" unsaved-value="0">
      <generator class="native" />
    </id>

02  <many-to-one name="Customer" class="Customer" 
       column="Customer_ID" not-null="true"/>
    <property name="Street" length="50" not-null="true" />
    <property name="City" length="50" not-null="true" />
    <property name="PostCode" length="10" not-null="true" />
    <property name="Country" length="50" not-null="true" />
  </class>
</hibernate-mapping>

In the Customer mapping, the private AddressSet collection is declared as a one-to-many collection of Address instances; we indicate that the Customer_ID field in the Address table is used as the link (line 01). In the Address mapping section, we also declare the Customer reference to use the same column name (line 02). This approach permits to navigate from the child back to the parent.

Let's demonstrate how easy it is to propagate the changes to our database; if we create a new test:

Image 5

and the configuration is set so the test is run using the NHibernate mode, then the test will generate the new schema for us, isn't that nice? Just remember to change the test App.config file:

Image 6

You may want to open a connection to the database to see the new schema:

Image 7

New address service

We are planning to modify the user interface so the following screens will be available:

Image 8

Image 9

Image 10

We need to provide a new service so we can create, retrieve, and update an Address instance:

Image 11

Adding a new service requires the following:

  • Add the new interface to the IContractLocator
  • There are three implementations of the interface that need to be updated
  • Add three new AddressServiceProxy, AddressServiceAdapter, and AddressWcfService classes

The implementation of the above classes is straightforward as they are in fact very similar to the implementations for the Customer service; you may want to get the source code for further details.

In the server side, we need to amend eDirectory.WcfService to add the new Address service to the list of endpoints:

XML
<configuration>
  ...
  <system.serviceModel>
    <services>
      ...
      <service name="eDirectory.WcfService.AddressWcfService" 
              behaviorConfiguration="eDirectory.WcfServiceBehaviour">
        <endpoint address="AddressServices" binding="basicHttpBinding" 
             bindingConfiguration="eDirectoryBasicHttpEndpointBinding" 
             contract="eDirectory.Common.ServiceContract.IAddressService" />
      </service>
    </services>
    ...
  </system.serviceModel>
  ...
</configuration>

Client side

Besides implementing the new classes AddressServiceAdapter and AddressServiceProxy

Image 12

we have added a new bunch of Views with their respective Models and ViewModels:

Image 13

Among the model classes, the one that needs to be mentioned is AgendaModel:

C#
class AgendaModel
{
    public IList<CustomerDto> CustomerList { get; set; }
    public CustomerDto SelectedCustomer { get; set; }
    public AddressDto SelectedAddress { get; set; }
}

Notice that the model provides class holders for the selected grid rows; this works in both ways, which is very nice. The only thing to do in the View is to set the binding correctly:

Image 14

It may not be obvious, but when the list of clients is retrieved from the server, each customer DTO contains a collection of addresses. You may implement a more chatty design where the address collection is only retrieved when the customer is selected. Also, the Customer reference in the Address class translates into the DTO implementation in storing the CustomerId instead; if you don't take this approach, the serialization of your DTOs would be a nightmare, to say the least:

Image 15

There is another interesting aspect on the AgendaViewModel, that is the way we manage the action buttons using the RelayCommand class. In this case, if a customer instance contains an address, the user needs to delete all addresses before the Delete button for the customer is enabled. This is achieved easily by implementing a predicate in the RelayCommand constructor using the above mentioned selected holder:

C#
private RelayCommand DeleteCustomerCommandInstance;
public RelayCommand DeleteCustomerCommand
{
    get
    {
        if (DeleteCustomerCommandInstance != null)
            return DeleteCustomerCommandInstance;
        DeleteCustomerCommandInstance = 
          new RelayCommand(a => DeleteCustomer(Model.SelectedCustomer.Id), 
          p => Model.SelectedCustomer != null && 
          Model.SelectedCustomer.Addresses.Count == 0);

        return DeleteCustomerCommandInstance;
    }
}

The XAML declaration is a piece of cake:

Image 16

Another aspect implemented is something that we have not had a chance to see before; this is how the ViewModel and Services use the selected customer DTO to enhance the user experience; for example, when a new customer instance is created, we need to ensure that the new customer instance is the one selected in the grid once the user is back to the main screen. We resolve this requirement as follows:

C#
public RelayCommand CreateCustomerCommand
{
    get
    {
        if (CreateCustomerCmdInstance != null)
            return CreateCustomerCmdInstance;
01      CreateCustomerCmdInstance = 
           new RelayCommand(a => OpenCustomerDetail(null));
        return CreateCustomerCmdInstance;
    }
}

private void OpenCustomerDetail(CustomerDto customerDto)
{
    var customerDetailViewModel = new CustomerDetailViewModel(customerDto);
02  var result = customerDetailViewModel.ShowDialog();
03  if (result.HasValue && result.Value)
        Model.SelectedCustomer = customerDetailViewModel.Model.Customer;
04  Refresh();
}

private void Refresh()
{
    long? customerId = Model !=null && Model.SelectedCustomer != null ? 
          Model.SelectedCustomer.Id : (long?) null;
    long? addressId = Model != null && Model.SelectedAddress != null ? 
          Model.SelectedAddress.Id : (long?)null;
    var result = CustomerServiceInstance.FindAll();
    Model = new AgendaModel { CustomerList = result.Customers };
    if(customerId.HasValue)
    {
05      Model.SelectedCustomer = 
          Model.CustomerList.FirstOrDefault(c => c.Id.Equals(customerId));
        ...
    }
    RaisePropertyChanged(() => Model);
}

There is a little bit of code above, but bear with us for a second; the CreateCustomerCommand delegates to the OpenCustomerDetail method (line 01), this method calls the customer detail screen and if a new customer instance is created, it sets the SelectedCustomer property in the Model (lines 02 and 03). Then the Refresh method is called which invokes the CustomerServiceInstance.FindAll() and sets the Model.SelectedCustomer (line 05) to the value it had before the service was called.

Chapter summary

Parent-child relationships are common in all applications; in this chapter, we discussed how relatively easy it is to implement those across all our application layers. We have discussed how to model our entities so collections are well managed. In summary, the parent is fully responsible for the creation and deletion of child instances. It is a good example of how our entities are moving away from just being simple CRUD data classes to more complex entities that implement business behavior.

We also discussed the NHibernate implementation and how easy it is at this point of the project creating new tests that automatically manage the new database schema, an aspect that proves to be invaluable. We also covered some MVVM techniques to leverage some common scenarios on the client side, like enable/disable action buttons using the predicates on the RelayCommand; once more, it was demonstrated how much value can be achieved by providing a rich model implementation to the XAML Views, reducing the amount of code-behind as a result of the XAML binding capabilities.

In the next chapter, we will discuss how easy it is to deploy our application to Microsoft Azure.

Appendix

Get the application running

For those that are new to the series or those who are not sure yet how to get the eDirectory application running, the following section describes the steps to quickly get the application running. eDirectory is an application that can be run in-memory or against a SQL Server database using an NHibernate repository implementation. Here, we discuss how to get the client running in a very easy manner: in-process in-memory mode.

In the first place, you need to verify that the client App.Config is set properly so SpringConfigFile is set to use the InMemoryConfiguration.xml file:

Image 17

Ensure that the eDirectory.WPF application is set to be the start up one:

Image 18

Change the configuration to the in-memory instance in Visual Studio:

Image 19

Now the only thing you need is to start the application: F5 or CTRL+F5:

Image 20

Couple WCF beauties

There are couple things done in this chapter on the WPF side that are worth a brief discussion. WCF by default terminates the client application when the first View that was created is closed. In this version of eDirectory, it is required to ask the user which View must be open. Once the user presses the OK button, the original screen must be closed; if nothing is done, the application terminates at that point. An easy way of stopping this behavior is to indicate to WPF that the application itself will look after its shutdown:

C#
public partial class App : Application
{
    
    public App()
    {
01      ShutdownMode = ShutdownMode.OnExplicitShutdown;
    }

    private void BootStrapper(object sender, StartupEventArgs e)
    {
        var boot = new eDirectoryBootStrapper();
        boot.Run();
02      Shutdown();
    }
}

When the App instance is created, it is indicated that the application will shutdown manually (line 01), which takes place after the Run method returns (line 02).

The second beauty is a customized enum converter that is used by the Selector View that permits matching a radio-button to a specific enum value. The converter is:

C#
public class EnumMatchToBooleanConverter : IValueConverter
{
01  public object Convert(object value, Type targetType, 
                  object parameter, CultureInfo culture)
    {
        if (value == null || parameter == null) return false;

        string checkValue = value.ToString();
        string targetValue = parameter.ToString();
        return checkValue.Equals(targetValue, 
                   StringComparison.InvariantCultureIgnoreCase);
    }

02  public object ConvertBack(object value, Type targetType, 
                  object parameter, CultureInfo culture)
    {
        if (value == null || parameter == null) return null;

        bool useValue = (bool)value;
        string targetValue = parameter.ToString();
        return useValue ? Enum.Parse(targetType, targetValue) : null;
    }
}

The Convert method is used to see if the radio-button must be set given an enumeration value; the method assumes that the radio-button is to be set if the parameter matches the passed value. ConvertBack returns null if the radio-button is not set; if it is set, it returns the enum value set in the XAML.

The XAML is as follows:

Image 21

The converter is declared as a resource named enumConverter and then used in the radio-button declaration; an enum value is assigned to each; CurrentOption is a ViewTypeEnum property declared on the ViewModel that is correctly set without any other additional code. Nice!

AutoMapper

In this chapter, we decided to introduce AutoMapper. This is an object-to-object mapper, and it is ideal for use when dealing with domain entities and DTOs. You may want to have a look at the CodePlex project for further details.

It is quite easy to use AutoMapper. In the first place, we create the mappings, then we install them and then the mappings can be used. In the eDirectory.Domain project, a new class is added that declares the mappings:

Image 22

Two mappings are defined, the mapping from Customer to CustomerDto is the interesting one. This one maps the DTO Addresses collection to a function that delegates into the other AutoMapper mapping to map the Addresses collection in the entity to a collection of AddressDto instances.

Then when the WCF service is started, the static Install method is invoked:

Image 23

You can also leverage the Spring.Net capabilities to initialise the static method by just declaring the class in the configuration file; this is the approach used when we execute the application in in-memory mode; this is another nice example of the Spring.Net capabilities:

Image 24

An example of how the eDirectory solution uses the AutoMapper mapping is found in the Customer service implementation:

Image 25

License

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


Written By
Software Developer (Senior)
Ireland Ireland
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionNHIbernate 3.2 and proxies Pin
Denis Kozlov5-Apr-12 10:29
Denis Kozlov5-Apr-12 10:29 
AnswerRe: NHIbernate 3.2 and proxies Pin
Enrique Albert5-Apr-12 12:49
Enrique Albert5-Apr-12 12:49 
QuestionStill more basic questions. Pin
tyzh11-Jan-11 16:43
tyzh11-Jan-11 16:43 
AnswerRe: Still more basic questions. Pin
Enrique Albert11-Jan-11 23:31
Enrique Albert11-Jan-11 23:31 
AnswerRe: Still more basic questions. Pin
Enrique Albert12-Jan-11 0:17
Enrique Albert12-Jan-11 0:17 
QuestionRe: Still more basic questions. Pin
tyzh12-Jan-11 6:35
tyzh12-Jan-11 6:35 
GeneralMy vote of 5 Pin
Vivek Johari9-Jan-11 4:03
Vivek Johari9-Jan-11 4:03 
GeneralMy vote of 5 Pin
Espen Harlinn7-Jan-11 3:33
professionalEspen Harlinn7-Jan-11 3:33 
QuestionSome basic questions Pin
tyzh5-Jan-11 17:50
tyzh5-Jan-11 17:50 
AnswerRe: Some basic questions Pin
Enrique Albert5-Jan-11 18:32
Enrique Albert5-Jan-11 18:32 
GeneralThanks, looks like I missed out on a lot of the fine details Pin
tyzh6-Jan-11 11:18
tyzh6-Jan-11 11:18 
GeneralRe: Thanks, looks like I missed out on a lot of the fine details Pin
Enrique Albert6-Jan-11 14:16
Enrique Albert6-Jan-11 14:16 
GeneralMy vote of 5 Pin
RaviRanjanKr31-Dec-10 2:50
professionalRaviRanjanKr31-Dec-10 2:50 
GeneralRe: My vote of 5 Pin
Enrique Albert2-Jan-11 12:10
Enrique Albert2-Jan-11 12:10 
GeneralMy vote of 5 Pin
prasad0222-Dec-10 3:48
prasad0222-Dec-10 3:48 
GeneralRe: My vote of 5 Pin
Enrique Albert2-Jan-11 12:10
Enrique Albert2-Jan-11 12:10 

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.