| | |
Chapter XII | | Chapter 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:
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:
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:
The following changes need to be added to the NHibernate mapping file:
<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:
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:
You may want to open a connection to the database to see the new schema:
New address service
We are planning to modify the user interface so the following screens will be available:
We need to provide a new service so we can create, retrieve, and update an Address
instance:
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:
<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
we have added a new bunch of Views with their respective Models and ViewModels:
Among the model classes, the one that needs to be mentioned is AgendaModel
:
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:
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:
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:
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:
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:
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:
Ensure that the eDirectory.WPF application is set to be the start up one:
Change the configuration to the in-memory instance in Visual Studio:
Now the only thing you need is to start the application: F5 or CTRL+F5:
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:
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:
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:
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:
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:
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:
An example of how the eDirectory solution uses the AutoMapper mapping is found in the Customer
service implementation: