Ask Kathleen

Digging Deeper into Silverlight and MEF

Silverlight applications based on the Managed Extensibility Framework and generalized user interfaces don't require hand coding every screen.

Last month I covered how to use the DataForm in the Silveright Toolkit to create a generalized UI with a single set of generalized user controls, producing an application from different business entities ("Mastering Silverlight," January 2010). As with many programming challenges, the devil is in the details, and I skipped a view model, patterns for using Managed Extensibility Framework (MEF) and some rather ugly gotchas when working with the DataForm. In this column I'll cover these issues, and next time I'll cover persistence using Windows Communication Foundation (WCF) services. You can find more information on the Model-View-View Model (MVVM) in one of my previous columns ("Applying Model-View-View Model in Silverlight," September 2009). Because this month's column relies heavily on MEF, you might want to review some of my other columns on the subject ("Working with MEF," April 2009, "Getting Current on MEF," October 2009, and "Stable Composition in MEF," November 2009).

One of the shortcuts I took in last month's column was to flatten or denormalize the data structure by including the customer name in the invoice class. While that's one approach to passing data across a service boundary to Silverlight, other approaches are more common. This month, I'll use a relational approach for parent data and include the CustomerId in the invoice data. This requires a lookup mechanism to display the customer name. I'll use this to illustrate one benefit of a view model: The data in the view model will enhance the model, including calculated and lookup fields.

Programmers new to MEF often overlook that there are two reasons to include classes in an MEF container. Allowing alternative implementations is one reason. But more importantly, a class's imports won't be satisfied unless the instance is created through the MEF container. You generally instantiate the class with the MEF container using the export/import pattern. I'll show how to leverage this with a contract pattern for logical entities. Logical entities are groups of classes that work together to provide functionality; examples include Customer and Invoice entities.

Last month's sample application included five separate projects: the server-side Web, the Silverlight application, common utilities, common contracts and business classes. See "Organizing Your Application," for an overview of factors driving project organization. This month I added two more projects: business contracts and view models. Separating the business contracts from the infrastructure contracts clarifies the difference, encourages experimental spiking and allows reuse.

As you develop MEF architectures, you'll find sets of functional parts that work together to supply the full behavior for each logical entity. For example, Invoice, Customer and Product entities may each include a business class, a service class, a view model and other classes. "Services" in this month's column will refer to singleton classes managing behavior, not WCF services.

A good way to organize these classes is to supply a non-generic interface that's task-based and a target type via MEF metadata: for example, an IViewModel interface with an IInvoice target type. The target type (IInvoice) is effectively a token that identifies the entity.

The non-generic interface allows retrieval where the type is known only by its name. The functionality of the interface may need to know the target type, in which case a derived interface can supply more type specificity:

Public Interface IViewModel ...
Public Interface IViewModel(Of T) ...

Types may include more than one export.

Providing a token that identifies the entity as a whole simplifies later operations. This brings up a question: What exactly is a logical entity, such as an Invoice? If you use the full set of currently defined properties, you exclude the possibility of an abbreviated set of columns representing the entity. As the functional entity will eventually include extensions such as validation and authorization, it's valuable to keep abbreviated objects part of the entity family. Using the full set of columns would also present challenges when columns are added or deleted because the interface changes would affect much of your application. The token could be an empty interface, but I prefer an interface that contains the properties that would always be part of the entity: the primary key and the human-friendly alternate key if one exists.

This splits the business contract into two pieces:

Public Interface IInvoice
   Inherits Contracts.ICoreBiz
   Property InvoiceId() As Int32
   Property InvoiceNumber() As String
End Interface
Public Interface IInvoiceStandard
   Inherits IInvoice
   Property CustomerId() As Int32
   ' More fields as needed
   ReadOnly Property LineItems() As IList( _
		Of IInvoice-LineItem)
End Interface

A custom export attribute can specify the task contract and the target type. Using a custom export is synonymous with using an Export attribute and a Metadata attribute. In this case, the BizService custom export attribute exports the IBizService interface and provides IInvoice as the target type:

<BizService(GetType(IInvoice))> _
Public Class InvoiceService
   Implements IBizService(Of IInvoice) 

Different Ways to Retrieve MEF Parts
Code in different parts of your application needs to retrieve parts based on the token. There are several scenarios for retrieval, many of which are likely to be included in your application. Code may want one instance or want to be able to create many new instances. It may know the specific type, the type as a generic or only knows the task/service type by name. The last is especially interesting because it allows generalized code.

The Import attribute imports one part when the containing instance is created. Imports must be public in Silverlight because Silverlight doesn't allow reflection access to private members:

   <Import()> _
   Public _customers As CustomerViewModel

PartCreator is unique to Silverlight and creates a factory that supplies new instances of the underlying part as requested:

   <Import()> _
   Public _creator As PartCreator(Of CustomerViewModel)

You can learn more about MEF behavior with generics in "Using Generics with MEF," p. 37. Because MEF exports the type as it exists at runtime, if either of these imports uses generics, the import contract will be the closed generic. If ViewModelBase -- which has a generic argument of TViewModel constrained to IViewModel -- and CustomerViewModel inherit from ViewModelBase and implement IViewModel, then both of the following imports work if there's an export in the container for IViewModel (Of CustomerViewModel):

   <Import()> _
   Public _customers As IViewModel( _ 
	  Of T)
   <Import()> _
   Public _creator As PartCreator( _
	  Of IViewModel(Of T))

Parts retrieved using the generic IViewModel contract will expose only properties and methods of that contract, not of the underlying type. This style is very useful for code in base classes, but not sufficient to access specific properties and methods of sibling view models.

Generalized code, such as generalized user interfaces, requires creating parts when only the name of the type is known. For example, the short type name of the token may be retrieved from the navigation query:

 _bizClassName = NavigationContext. _
	  QueryString(argName)
 _searchForm = _searchFactory. _
	  GetItem(_bizClassName)

The search factory retrieves a class with the non-generic task contract ISearch and a target type named bizClassName. The search factory must first retrieve the target type based on the class name, and then filter the full collection of ISearch parts using LINQ.

Retrieving a type based on a class name is not trivial in Silverlight. You can't just grab a set of assemblies and iterate through them. I solved this by providing a contract named IBizContractAssemblyMarker. Placing the marker contract on a class in the assembly exposes it to the TypeRetriever class:

<Export(GetType(Common.IBizContractAssemblyMarker))> _
Public Class BizContractAssemblyMarker
   Implements Common.IBizContractAssemblyMarker
End Class

TypeRetriever contains an ImportMany for these markers. It looks for requested types within the assemblies where the markers reside:

      For Each marker In bizClassAssemblyMarkers
         Dim assembly = marker.Value.GetType.Assembly
         Dim q = assembly.GetTypes.Where( _
		    Function(t) t.Name = name)
         If q.count > 0 Then
            Return q.First
         End If
      Next

Unique factories for each task type, such as the _searchFactory, become tedious, so I created a generalized solution employing generic tricks that you can find at VisualStudioMagazine.com/Dollard0210, along with an explanation on my blog.

After importing an ITargetTypeRetriever (_retriever), parts following the task type/entity token target type pattern can be retrieved:

_viewModelMany = _retriever.GetItem( _
Of IViewModelMany)(targetType, False)

The final Boolean attribute specifies whether a default value should be used if available. A default value is the task type with a target type of null (Noting in Visual Basic). This allows functionality such as edit or search to be generalized with the default or specialized to the entity.

Different parts of your application may retrieve parts in different ways based on the information available and on the level of access required.

Implementing the View Model
The view model provides abstraction between the view and the model, allowing them to independently change. This is especially appropriate with the heavy use of data binding that occurs in a generalized UI. Binding is not only to the data fields, but also the general state of the object, such as whether it's read-only.

The view models also provide lookup values and a calculated field. Most view model properties directly wrap the corresponding model properties:

   Public Property InvoiceNumber() As String
      Get
         Return Model.InvoiceNumber
      End Get
      Set(ByVal value As String)
         Model.InvoiceNumber = value
      End Set
   End Property

The lookup fields need a bit more work. Implementations of both IViewModel and the IViewModelMany are non-shared. Using the IViewModelMany implementation would be inefficient because it would result in multiple copies of the list of customers or products. A new interface allows a simplified implementation exposed as a shared part:

<ViewModelLookup(GetType(ICustomer))> _
<Export(GetType(IViewModelLookup(Of ICustomer)))> _
<PartCreationPolicy(CreationPolicy.Shared)> _
Public Class CustomerViewModelLookup
   Inherits ViewModelManyBase(Of ICustomer, _
	  CustomerViewModel)
   Implements IViewModelLookup(Of ICustomer)

The IViewModelLookup interface contains a LookupDisplay method which returns a display string for a passed id. My implementation creates an empty CustomerViewModelMany:

   <Import()> _
   Public _list As CustomerViewModelMany
   Private _isFilled As Boolean

The LookupDisplay method fills the customer list if needed, finds the matching customer id and returns the customer name:

Public Function LookupDisplay(ByVal id As Int32) _
               As String _
               Implements IViewModelLookup.Lookup
      If Not _isFilled Then
         _list.RetrieveMany(Nothing)
         _isFilled = True
      End If
      Dim customer = (From c In _list Where _
		  c.CustomerId = id).FirstOrDefault
      If customer IsNot Nothing Then
         Return customer.CustomerName
      End If
      Return "Customer ID not found: " & CStr(id)
   End Function

I just grabbed the entire collection to illustrate the approach. You can provide a more sophisticated strategy if needed. This code is designed for synchronous operations, and I'll return to it next time in relation to Silverlight persistence behavior and asynchronous operations.

Solving Issues with DataForm and MEF
The DataForm mechanism for scrolling through records works well, but adding and deleting items needs tweaking. By default, DataForm's delete operation simply removes the item from the collection. We need to mark items as deleted and update the database, either immediately or later in a transaction or batch. The DataForm.DeletingItem event lets you specify deletion behavior and optionally cancel deletion:

   Private Sub DataForm_DeletingItem(ByVal sender _
	  As Object, ByVal e As CancelEventArgs)
      Dim current = CType(DataForm.CurrentItem, _
		  IViewModel)
      current.Delete()
   End Sub

Adding items is more problematic. By default, DataForm only lets you add items if the collection is of a type that has a parameterless constructor in scope. If MEF parts are created using a constructor, their imports won't be satisfied. While SatisfyImports can be called in some situations, it can't be used on parts that have an export attribute, and that attribute is required for other MEF construction. Therefore, it's essential that parts be created using the MEF container and not directly through constructors.

The DataForm relies on the contained collection to do much of its work. When working with ordinary collections, the DataForm provides a view, which is a wrapper for your collection. DataForm uses the PagedCollectionView wrapper unless your collection supports ICollectionView and IEditableCollectionView.

Of course, you could implement these interfaces on your collection, but this is not a trivial undertaking -- and changes to PagedCollectionView to support MEF are relatively small and provided in this article's accompanying download. I changed the CanConstructItem method to check for MEF exports using a new MefSupport class. I also changed AddNew to call MefSupport, as well. To be consistent with the PagedCollectionView, the new MefSupport class is written in C#. It contains similar Create and CanCreate static methods. Each receives the target type of the inner collection as a parameter. It uses reflection to create a MethodInfo object for a private generic method within the class, construct the closed generic method with the passed target type and invoke the method:

static internal bool CanCreate(Type type)
{ var methodInfo = typeof(MefSupport).GetMethod(
       "CanCreateInternal", BindingFlags.Static | 
       BindingFlags.NonPublic );
  var genericMethod = methodInfo.
	 MakeGenericMethod(new Type[] { type });
  return (bool)genericMethod.Invoke(null, null);
}

The private method declares a local variable with the generic argument of the target type:

static private bool CanCreateInternal<T>()
{ var x = new MefSupportInternal<T>();
  return x.CanCreate();
}

The nested MefSupportInternal class includes an import for a PartCreator of the type argument T, which is the target type passed to the CanCreate method. The constructor calls SatisfyImports, allowing MEF to resolve the PartCreator. If it's successful, CanCreate returns true and Create returns the requested type:

      public class MefSupportInternal<T>
      {
         [Import(AllowDefault=true)]
         public PartCreator<T> _creator;

         public MefSupportInternal()
         { PartInitializer.SatisfyImports(this); }

         public bool CanCreate()
         { return _creator != null; }

         public object Create()
         { return _creator.CreatePart().ExportedValue; }
      }

To use the custom PagedCollectionView, wrap your collection before passing it to the DataForm:

      Me.DataContext = New PagedCollectionViewMef(_
		  viewModelMany)

A couple of other changes: The DataAnnotations attributes fail as you cross multiple boundaries -- such as providing the view model layer -- so they no longer work. I'll return to this issue in a future column. Rather than directly instantiating a DataForm, I wrapped a DataForm in a user control for a consistent appearance with handcrafted user controls.

You can download the sample application to explore these details further. While there are a number of challenges, the payoff is an application that you can build quickly and evolve during development and the remainder of the project lifecycle.

comments powered by Disqus
Upcoming Events

.NET Insight

Sign up for our newsletter.

I agree to this site's Privacy Policy.