C H A P T E R 9
■ ■ ■
185
Application Support
So far, this book has covered the individual layers of the MVVM architecture—model, view, and
ViewModel—in sufficient detail to create an application employing this pattern. There are some
remaining modules of important functionality that have been omitted thus far.
This chapter will plug those holes with the glue that binds together the three aforementioned layers.
This is the application support (henceforth app support) that covers a whole gamut of extra functionality
that does not sit comfortably in any of the established layers of MVVM. This chapter will deal with four of
these modules that are most commonly required in a modern WPF or Silverlight application.
Topics covered in this chapter are: the serialization of the object graph, implementing a data access
layer, allowing users to configure the behavior, and settings of the application, and adding extensibility
via plug-ins.
The diagram in Figure 9–1 shows how the layers are organized when app support layers are added to
the architecture. The arrows indicate the direction of the dependencies.
Figure 9–1. The MVVM architecture with app support layers in place
It is more common for app support functionality to sit between the ViewMmodel and model than
between the view and ViewModel! This is because there are more areas that require wrapping in view-
model classes for consumption by the view, yet are not strictly part of the model itself.
CHAPTER 9
■
APPLICATION SUPPORT
186
Another pertinent point of notice is that each of these layers need not be implemented as single
assemblies. On the contrary, it makes more organizational sense to split app support functionality into
separate modules both to facilitate reuse and to maintain a strict focus on the single responsibility
principle. Furthermore, if a plug-in architecture is implemented early, it can be leveraged to include
functionality that would otherwise be built in to the core of the application. Of course, caution must be
taken with a plug-in architecture as it is no trivial task. Its implementation must be fully planned,
estimated, and—most importantly—justified with a business case.
Serialization
Serialization is the term applied to the process of storing the state of an object. Deserialization is the
opposite: restoring an object’s state from its stored format. Objects can be serialized into binary format,
an XML format, or some tertiary, purpose-built format if required. This section deals primarily with
binary serialization that can be used to save the object to persistent storage, such as a hard drive, to
enable the object to be deserialized at a later date. Binary serialization is also used for transmitting
objects over a process or network boundary so that the object’s state can be faithfully recreated on the
receiving end.
An object graph is a directed graph that may be cyclic. Here, the term graph is intended in its
mathematical definition: a set of vertices connected by edges. It is not to be confused with the more
common use of the word graph which is shorthand for the graph of a function. In an object graph, the
vertices are instances of classes and the edges represent the relationships between the classes, typically
an ownership reference.
Serialization operates on a top-level object and navigates each object, saving the state of value types
such as string, int, or bool, and then proceeding down through the other contained objects where the
process continues. This process continues until the entire graph has been saved. The result is a replica of
the graph that can be used to recreate each object and their relationships at a later date.
In an MVVM application, the model will be serialized, most commonly to save its current state to
disk to be loaded again later. This allows the user to stop what they are currently doing and return to the
application whenever it is next convenient to them, yet have their current work available on demand.
Serializing POCOs
There are a number of options for serializing the model, and each has its respective strengths and
weaknesses. All of these methods are part of the .NET Framework, which performs all of the heavy-
lifting. Client applications need to provide some hints to the serialization classes so that they can
properly create a replica of the object graph.
These hints come in three forms: implicit, explicit, and external. Implicit and explicit serialization
both require alterations to be made directly on the model classes. They differ in how much control they
afford the classes in describing themselves and their structure to the serialization framework. External
serialization can be performed on any class, even those that are marked as sealed and have no avenues
for extension or alteration. Although external serialization may require intimate knowledge of the
internal implementation of a class, its benefits may outweigh this cost.
Invasive Serialization
There are two ways of enabling serialization on an object. Firstly, the
SerializableAttribute
can be
applied to the class, as exemplified in Listing 9–1.
CHAPTER 9 ■ APPLICATION SUPPORT
187
Listing 9–1. Marking a Class as Serializable
[Serializable]
public class Product
{
public Product(string name, decimal price, int stockLevel)
{
Name = name;
Price = price;
StockLevel = stockLevel;
}
public string Name
{
get;
private set;
}
public decimal Price
{
get;
private set;
}
public int StockLevel
{
get;
private set;
}
}
This is extremely trivial and, although technically invasive, does not require a great deal of alteration
to the class. As might be expected, this is a semantic addition to the class and does not really add any
extra functionality; it just allows the class to be serialized by the framework. Omitting this attribute
yields a SerializationException when an attempt is made to serialize the class, so it is akin to a
serialization opt-in mechanism.
■ Note Be aware that the
Serializable
attribute is a requirement for every object in the graph that is to be
serialized. If a single class is not marked as
Serializable
, the whole process will fail, throwing a
SerializationException
.
There is more work required to actually perform the serialization, as shown in Listing 9–2.
Listing 9–2. Serializing the Product Class
public void SerializeProduct()
{
Product product = new Product("XBox 360", 100.00, 12);
CHAPTER 9 ■ APPLICATION SUPPORT
188
IFormatter formatter = new BinaryFormatter();
Stream stream = new FileStream("Product.dat", FileMode.Create, FileAccess.Write,
FileShare.None);
formatter.Serialize(stream, product);
stream.Close();
}
First of all, the product instance is created. An IFormatter implementation, here the
BinaryFormatter, is also instantiated. The IFormatter knows how to take data from the objects and
transform them into another format for transmission or storing. It also knows how to perform
deserialization, ie: loading the objects back to their former state from the storage format. The
BinaryFormatter is one implementation of this interface, outputting binary representations of the
underlying data types.
■ Tip There is also the
SoapFormatter
implementation that serializes and deserializes to and from the SOAP
format. The
IFormatter
can be implemented to provide a custom format if it is required, but it may help to
subclass from the abstract
Formatter
class, which can ease the process of developing customer serialization
formatters.
Formatters write the output data to streams, which allows the flexibility to serialize to files with the
FileStream, in-process memory using the MemoryStream or across network boundaries via the
NetworkStream. For this example, a FileStream is used to save the product data to the Product.dat file.
The serialization magic happens in the Serialize method of the chosen IFormatter implementation, but
don’t forget to close all streams when the process is finished.
Deserialization is trivially analogous, as shown in Listing 9–3.
Listing 9–3. Deserializing the Product Class
public void DeserializeProduct()
{
IFormatter formatter = new BinaryFormatter();
Stream stream = new FileStream("Product.dat", FileMode.Open, FileAccess.Read,
FileShare.Read);
Product product = (Product) formatter.Deserialize(stream);
stream.Close();
}
Note that the IFormatter.Deserialize method returns a vanilla System.Object that must be cast to
the correct type.
Hold on, though. The Product class definition indicated that the three properties had private setters,
so how can the deserialization process inject the correct values into the Product? Note also that there is
no default constructor because it was overridden to provide initial values for the immutable properties.
The serialization mechanism circumvents these problems using reflection, so this example will work as-
is without any further scaffolding. Similarly, private fields are serialized by default.
More control over the process of serializing or deserializing may be required, and this is provided for
by the ISerializable interface. There is only one method that requires implementing, but a special
constructor is also necessary to allow deserializing (see Listing 9–4). The fact that constructors cannot be
contracted in interfaces is a shortcoming of the .NET Framework, so be aware of this pitfall.
CHAPTER 9 ■ APPLICATION SUPPORT
189
■ Tip It is not necessary to implement the
ISerializable
interface if all that is required is property or field
omission. For this, mark each individual field or property with the
NonSerializable
attribute to omit it from the
serialization process.
Listing 9–4. Customizing the Serialization Process
[Serializable]
public class Product : ISerializable
{
public Product(string name, decimal price, int stockLevel)
{
Name = name;
Price = price;
StockLevel = stockLevel;
}
protected Product(SerializationInfo info, StreamingContext context)
{
Name = info.GetString("Name");
Price = info.GetDecimal("Price");
StockLevel = info.GetInt32("StockLevel");
}
[SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("Name", Name);
info.AddValue("Price", Price);
info.AddValue("StockLevel", StockLevel);
}
public string Name
{
get;
private set;
}
public decimal Price
{
get;
private set;
}
public int StockLevel
{
get;
private set;
}
}
CHAPTER 9 ■ APPLICATION SUPPORT
190
The class describes its structure to the SerializationInfo class in the GetObjectData method and is
then serialized. The labels that were used to name each datum are then used in the custom constructor
to retrieve the relevant value during deserialization. The deserialization constructor is marked as
protected because the framework finds it via reflection yet it is otherwise hidden from consumers of the
class. The GetObjectData method is marked with the SecurityPermission attribute because serialization
is a trusted operation that could be open to abuse.
The problem with this custom serialization is that the class is no longer a POCO: it is a class that is
clearly intended to be serialized and has that requirement built-in. Happily, there’s a way to implement
serialization with being so invasive.
External Serialization
The benefits and drawbacks of invasive serialization versus external serialization are a choice between
which is most important to enforce: encapsulation or single responsibility. Invasive serialization
sacrifices the focus of the class in favor of maintaining encapsulation; external serialization allows a
second class to know about the internal structure of the model class in order to let the model perform its
duties undistracted.
Externalizing serialization is achieved by implementing the ISerializationSurrogate interface on a
class dedicated to serializing and deserializing another (see Listing 9–5). For each model class that
requires external serialization, there will exist a corresponding serialization surrogate class.
Listing 9–5. Implementing External Serialization for the Product Class
public class ProductSurrogate : ISerializationSurrogate
{
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
{
Product product = obj as Product;
if (product != null)
{
info.AddValue("Name", product.Name);
info.AddValue("Price", product.Price);
info.AddValue("StockLevel", product.StockLevel);
}
}
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
public object SetObjectData(object obj, SerializationInfo info, StreamingContext
context, ISurrogateSelector selector)
{
Type productType = typeof(Product);
ConstructorInfo productConstructor = productType.GetConstructor(new Type[] {
typeof(string), typeof(decimal), typeof(int) });
if (productConstructor != null)
{
productConstructor.Invoke(obj, new object[] { info.GetString("Name"),
info.GetDecimal("Price"), info.GetInt32("StockLevel") });
}
return null;
}
}
CHAPTER 9 ■ APPLICATION SUPPORT
191
The interface requires two methods to be fulfilled: GetObjectData for serializing and SetObjectData
for deserializing. Both must be granted security permissions in order to execute, just as with the
ISerializable interface. The object parameter in both cases is the model object that is the target of this
serialization surrogate. A SerializationInfo instance is also provided to describe the object’s state and
to retrieve it on deserialization. SetObjecData, in this example, uses reflection to discover the constructor
of the Product class that accepts a string, decimal, and int as parameters. If found, this constructor is
then invoked and passed the data retrieved by the serialization framework. Note that the return value for
the SetObjectData method is null: the object should not be returned as it is altered through the
constructor invocation.
The reflection framework allows the serialization code to deal with very defensive classes that,
rightly, give away very little public data. As long as the fields are known by name and type, they can be
retrieved, as shown in Listing 9–6.
Listing 9–6. Retrieving a Private Field Using Reflection
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
{
Product product = obj as Product;
if (product != null)
{
// ...
// find the private float field '_shippingWeight'
Type productType = typeof(Product);
FieldInfo shippingWeightFieldInfo = productType.GetField("_shippingWeight",
BindingFlags.Instance | BindingFlags.GetField | BindingFlags.NonPublic);
float shippingWeight = (float)shippingWeightFieldInfo.GetValue(product);
info.AddValue("ShippingWeight", shippingWeight);
}
}
The BindingFlags specify what sort of data the reflection framework is looking for, which in this case
is a private instance field.
In order to serialize the product with this external serializer, the formatter that is used must be
furnished with the surrogate, as shown in Listing 9–7.
Listing 9–7. Serializing Using a SurrogateSelector
public void SerializeProduct()
{
Product product = new Product("XBox 360", 100.00M, 12);
IFormatter formatter = new BinaryFormatter();
SurrogateSelector surrogateSelector = new SurrogateSelector();
surrogateSelector.AddSurrogate(typeof(Product), new
StreamingContext(StreamingContextStates.All), new ProductSurrogate());
formatter.SurrogateSelector = surrogateSelector;
Stream stream = new FileStream("Product.dat", FileMode.Open, FileAccess.Read,
FileShare.None);
Product product = (Product)formatter.Deserialize(stream);
}
The addition is linking the Product type with the ISerializationSurrogate implementation that will
be used to serialize and deserialize each instance that occurs in the object graph. The StreamingContext
class is used throughout the serialization framework to describe the source or destination of the
deserialization or serialization process, respectively. It is used here to allow linking multiple surrogates
CHAPTER 9 ■ APPLICATION SUPPORT
192
that target different sources or destinations, so the Product could, in theory, be serialized by two
different surrogates, one for remoting the object and one for saving the object to a file. The
StreamingContext.Context property can also be set in the serialization code and read from within the
GetObjectData or SetObjectData methods of the ISerializationSurrogate implementation to inject a
dependency or to provide extra settings, for example.
Note that the serialization code has now been fully separated from the Product class. In fact, it need
not even be marked with the Serializable attribute. This allows the serialization code to live in a
separate assembly that depends up the Model assembly (or assemblies) and is, in turn, depended upon
by the ViewModel assembly.
Extensibility
As discussed earlier in this book, application code is typically separated into assemblies that each deal
with specific functionality that the application requires. It is possible to take this one step further: avoid
linking the assemblies statically and, instead, have some of the assemblies loaded dynamically at run-
time. The application is then split conceptually into a “host” and a number of “extensions.” Each
extension can provide additional functionality to the host and can be changed and redeployed
independently of the host.
■ Note As of version 4, Silverlight now has access to the Managed Extensibility Framework that is covered in this
section. Silverlight applications can now benefit from extensibility just as much as their WPF brethren.
Why Extend?
There are many compelling reasons to allow your application to be extended, and a few of these will be
covered here. First, though, a short warning: enabling the ability to extend an application should not be
taken lightly. Although the framework covered in this section that allows extensions to be loaded is very
simple, thought must still be given to where extensions can occur in the application, and this diverts
resources from adding direct value to the product. Unless there is a strong case for supporting
extensibility, it is more than likely that it should not be undertaken.
Natural Team Boundaries
Software development teams are increasingly spread across geographically disparate locations. It is not
uncommon to have teams in Europe, Asia, and North America all working on different parts of the same
application. One way to separate the responsibilities of each team is to allocate one team to be the host
developers and split the rest of the application’s functionality into extensions that teams can work on
almost in isolation.
Good communication lines and a high level of visibility are required to ensure that such
intercontinental development succeeds. If the host application developers can expose the right
extension points for other teams, then they can diligently work on their section of the application
without constantly seeking approval or answers from a central authority. Each team becomes
accountable for their extension and claims ownership of it, taking praise and criticism for its good and
bad parts, respectively.