Introduction
Currently, there's no official MVC/MVP/MV-VM frameworks for Silverlight. When developing enterprise applications, or building a large scale LOB application in Silverlight, client side architecture becomes important for "development salability". Although there is some guidance or frameworks for WPF, none of them can be easily applied to Silverlight. Adobe's Cairngorm has been broadly used in the Flex RIA application since 2006; it has easy-to-understand concepts, well-recognized design patterns, and has proved to work well to scale large line-of-business applications' development. This article describes the efforts of porting Cairngorm to Silverlight (Beta 2) in Visual Studio 2008 SP1, provides details about which concepts/classes have been adopted and what has been dropped, and also includes a sample application to demonstrate how it works and how it's intended to use. It helped me a lot to create a Silverlight prototype at work for a potential large scale consumer-oriented financial application; wish this effort would be useful to other Silverlight developers.
Brief on Cairngorm
Cairngorm is the lightweight micro-architecture for Rich Internet Applications built in Flex or AIR developed by Adobe Consulting. Its target applications are Enterprise RIA or medium to large scale LOB RIA. As detailed in the Introducing Cairngorm document, the major benefits of Cairngorm are:
- Adding new features or changes to existing features are easier: new features can be "plugged in" by adding a new View, Model, Event, Command, and Delegate (note: not .NET Delegate, it refers to Cairngorm Delegate) without changing/affecting other features
- Enables agile team development process: designers (Views), front end developers (Model, Events, Commands, Delegates, Data Binding), and data-service developers (Web Services) can work in parallel
- Easier maintenance and debugging, also easier unit-test business logic codes.
Cairngorm helps developers to identify, organize, and separate code based on its roles/responsibilities; at the highest level, it has the following primary components:
- Model holds data objects and the state of the data via
ModelLocator
- Controller handles Cairngorm Events and executes the corresponding
Command
class via FrontController
- Commands are non-UI components that process business logic, it usually implements both the
ICommand
and the IResponder
interfaces
- Events are custom events that trigger business objects (i.e. Commands) to start processing, normally raised by a View's event (application events, user input events, etc.) handlers
ServiceLocator
is a repository of pre-configured client/server communication components
- Cairngorm Delegates are classes that know how to communicate with Web Services and route Result and Fault events to a Command via the
IResponder
interface
- Views renders Model's data and communicates with the Controller using Events, it also monitors Model data changes by Data Binding
More on Cairngorm can be found at Cairngorm Developer Documentation.
What's Changed in the Silverlight Cairngorm
When implementing Cairngorm for Silverlight, all primary concepts/components are preserved except ServiceLocator
. Details follow:
1. No need for ServiceLocator, using WebClient in Delegate Directly
Since in Silverlight, the WebClient
class and the generated service proxy are used to interact with services in an asynchronized and strong-typed way, it's more convenient for Cairngorm Delegate objects to instantiate a WebClient
or a generated service proxy and use them directly, it eliminates the need for the ServiceLocator
.
However, in the source code, I still include a C# definition for the IServiceLocator
interface and an abstract C# class for the ServiceLocator
, you can find them in the Business sub-folder ---- if you ever need them.
[Update Notes 9/14/2008] A thread-safe implementation of Silverlight Cairngorm v0.0.0.1 has removed ServiceLocator
and IServiceLocator
interface from the source code, it can be downloaded in another article here.
2. ModelLocator becomes abstract and implements INotifyPropertyChanged
The implementation of the INotifyPropertyChanged
interface in the abstract ModelLocator
frees the derived application ModelLocator
(in the sample app, it's the SilverPhotoModel
class) to implement the interface, and makes sure the derived Model is "bindable" to the View (XAML). I really wish future versions of Silverlight would have something like the [Bindable]
attribute just as the Flex ActionScript code does, then developers don't need to worry about the INotifyPropertyChanged
interface and call NotifyPropertyChanged("PropertyName")
in all the setter methods, it's a job more suitable for the compiler. Then, designing the Model and making it bindable will be much easier.
public abstract class ModelLocator : INotifyPropertyChanged
{
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
The application Model class will derive from the ModelLocator
listed above. The derived class will implement the Singleton pattern and will also need to call NotifyPropertyChanged
for the public
bindable property's setter methods. You'll see an example in the sample application.
[Update Notes 9/14/2008] A thread-safe implementation of ModelLocator
and INotifyPropertyChange
interface is in another article here.
3. FrontController becomes abstract and CairngormEventDispatcher becomes internal
Making CairngormEventDispatcher internal
will actually simplify the application code to raise a Cairngorm event: it is forced not to access CairngormEventDispatcher
; instead, just instantiate a CairngormEvent
object, then call its Dispatch
method. CairngormEventDispatcher
is only internal in the Cairngorm assembly, the application does need to care about it.
namespace SilverlightCairngorm.Control
{
internal class CairngormEventDispatcher
{
private static CairngormEventDispatcher instance;
public static CairngormEventDispatcher getInstance()
{
if ( instance == null )
instance = new CairngormEventDispatcher();
return instance;
}
private CairngormEventDispatcher()
{
}
public delegate void EventDispatchDelegate
(object sender, CairngormEventArgs args);
public event EventDispatchDelegate EventDispatched;
public void dispatchEvent(CairngormEvent cairngormEvent)
{
if (EventDispatched != null)
{
CairngormEventArgs args = new CairngormEventArgs(cairngormEvent);
EventDispatched(null, args);
}
}
}
}
Cairngorm FrontController
is preserved in the Silverlight Cairngorm:
namespace SilverlightCairngorm.Control
{
public abstract class FrontController
{
private Dictionary eventMap = new Dictionary();
public FrontController()
{
CairngormEventDispatcher.getInstance().EventDispatched +=
new CairngormEventDispatcher.EventDispatchDelegate(ExecuteCommand);
}
void ExecuteCommand(object sender, CairngormEventArgs args)
{
if (eventMap.ContainsKey(args.raisedEvent.Name))
{
eventMap[args.raisedEvent.Name].execute(args.raisedEvent);
}
}
public void addCommand(string cairngormEventName, ICommand command)
{
eventMap.Add(cairngormEventName, command);
}
}
}
4. Added new abstract class: CairngormDelegate
All CairngormDelegate
classes will derive from this class; it requires all derived classes to have a reference to IResponder
.
namespace SilverlightCairngorm.Business
{
public abstract class CairngormDelegate
{
protected IResponder responder;
protected CairngormDelegate(IResponder responder)
{
this.responder = responder;
}
}
}
5. Classes/Interfaces that were removed from SilverlightCairngorm
The IValueObject
interface and the ValueObject
type have been removed. Because, most likely, those data transfer objects will be generated.
ViewHelper
and ViewLocator
are also not in SilverlightCairngorm
, because it's not a good idea to let model access view directly in data-binding scenario;
SequenceCommand
is not ported, in the case of chained events/commands, a Cairngorm Event could be raised from the onResult
method in the Command.
HTTPService
, WebService
, and RemoteObject
are all Flex specific, and not included in SilverlightCairngorm
.
Silverlight Cairngorm Sample Application
This sample application demonstrates how to use Cairngorm in a Silverlight application. If you have Silverlight 2 Beta 2 installed, you can run the app from here. It's very simple: to allow the user to type in a term to search photos using FlickR REST API, bind the search result to the left-hand navigation list, then automatically display the first image in the right hand side. Of course, when the user selects an image in the list, the display image will be updated.
1. Define the View in XAML
The auto-generated Page.xaml is extended to have the application layout, it has a simple text animation for the application title. It also references three UserControl
s in its layout markup. The idea is Page.xaml just provides an entry point and the application layout for the View. The actual functional Views are defined by the UserControl
to accommodate potential UI design changes from the designer.
All the UserControl
s are defined in the View sub-folder and have SilverlightCairngormDemo.View
as their namespace. PhotoSearch.xaml just has a textbox for the user to input the search term and a "Go" button to trigger the search photo action. PhotoList.xaml has a ListBox
and an ItemTemplate
to render the search result as a text (Photo's Title) list. PhotoSearch
and PhotoList
are stacked vertically in Page.xaml as the left-hand side navigation list.
ContentZone.xaml just contains an Image
control to display the selected photo.
2. Define the SearchPhotoDelegate
Let's start to use SilverlightCairngorm
from bottom to top. By deriving from CairngormDelegate
, the SearchPhotoDelegate
class is the only class that understands how to communicate with the FlickR REST API ---- sending a request and routing onResult
and onFault
calls to IResponsder
:
namespace SilverlightCairngormDemo.Business
{
public class SearchPhotoDelegate : CairngormDelegate
{
public SearchPhotoDelegate(IResponder responder)
: base(responder)
{
}
public void SendRequest(string searchTerm)
{
string apiKey = "[[You can get your API key for free from FlickR]]";
string secret = "[[Yours goes here]]";
string url = String.Format("http://api.flickr.com/services/rest/?" +
"method=flickr.photos.search&api_key={1}&text={0}",
searchTerm, apiKey, secret);
WebClient flickRService = new WebClient();
flickRService.DownloadStringCompleted +=
new DownloadStringCompletedEventHandler(
flickRService_DownloadStringCompleted);
flickRService.DownloadStringAsync(new Uri(url));
}
private void flickRService_DownloadStringCompleted(object sender,
DownloadStringCompletedEventArgs e)
{
if (null != e.Error)
responder.onFault("Exception! (" + e.Error.Message + ")");
else
responder.onResult(e.Result);
}
}
}
3. Define the SearchPhotoCommand
SearchPhotoCommand
implements the IResponder
and ICommand
interfaces. ICommand.execute
will be invoked when the controller processes a Cairngorm event, and IResponder.onResult
will be called from SearchPhotoDelegate
when no exception occurs. IResponder.onFault
handles exceptions and errors from SearchPhotoDelegate
or invalid data:
namespace SilverlightCairngormDemo.Command
{
public class SearchPhotoCommand : ICommand, IResponder
{
private SilverPhotoModel model = SilverPhotoModel.getInstance();
#region ICommand Members
public void execute(CairngormEvent cairngormEvent)
{
string toSearch = model.SearchTerm;
SearchPhotoDelegate cgDelegate = new SearchPhotoDelegate(this);
cgDelegate.SendRequest(toSearch);
}
#endregion
#region IResponder Members
public void onResult(object result)
{
string resultStr = (string)result;
if (String.IsNullOrEmpty(resultStr))
{
onFault("Error! (Server returns empty string)");
return;
}
XDocument xmlPhotos = XDocument.Parse(resultStr);
if ((null == xmlPhotos) ||
xmlPhotos.Element("rsp").Attribute("stat").Value == "fail")
{
onFault("Error! (" + resultStr + ")");
return;
}
model.PhotoList = xmlPhotos.Element("rsp").Element(
"photos").Descendants().Select( p => new FlickRPhoto
{
Id = (string)p.Attribute("id"),
Owner = (string)p.Attribute("owner"),
Secret = (string)p.Attribute("secret"),
Server = (string)p.Attribute("server"),
Farm = (string)p.Attribute("farm"),
Title = (string)p.Attribute("title"),
} ).ToList<flickrphoto />();
if (model.PhotoList.Count > 0)
model.SelectedIdx = 0;
else
onFault("No such image, please search again.");
}
public void onFault(string errorMessage)
{
model.SelectedIdx = -1;
model.PhotoList = new List<flickrphoto />()
{ new FlickRPhoto() { Title = errorMessage } };
}
#endregion
}
}
[Update Notes 9/14/2008] An updated SearchPhotoCommand
is provided in another article here, it uses thread-pool thread to perform XML parsing and data transformation to object collections to test the thread-safty of ModelLocator
.
4. Define the SilverPhotoController
SilverPhotoController
derives from FrontController
, registers a specific event name with SearchPhotoCommand
, and routes the event to the corresponding Command at runtime.
namespace SilverlightCairngormDemo.Control
{
public class SilverPhotoController : FrontController
{
public const string SC_EVENT_SEARCH_PHOTO = "cgEvent_SearchPhoto";
private static SilverPhotoController instance;
public static SilverPhotoController getInstance()
{
if ( instance == null )
instance = new SilverPhotoController();
return instance;
}
private SilverPhotoController()
{
base.addCommand(SC_EVENT_SEARCH_PHOTO, new SearchPhotoCommand());
}
}
}
5. Define the Model
The first type in the Model is the FlickRPhoto
class; it represents a photo data object coming back from the search.
namespace SilverlightCairngormDemo.Model
{
public class FlickRPhoto
{
public string Id { get; set; }
public string Owner { get; set; }
public string Secret { get; set; }
public string Server { get; set; }
public string Farm { get; set; }
public string Title { get; set; }
public string ImageUrl
{
get
{
if (String.IsNullOrEmpty(Farm) || String.IsNullOrEmpty(Server) ||
String.IsNullOrEmpty(Id) || String.IsNullOrEmpty(Secret))
return null;
return string.Format
("http://farm{0}.static.flickr.com/{1}/{2}_{3}.jpg",
Farm, Server, Id, Secret);
}
}
}
}
Now, it's time to define SilverPhotoModel
; it derives from ModelLocator
and is implemented as a Singleton. Please also notice the NotifyPropertyChanged("PropertyName")
calls, those are crucial for data binding.
namespace SilverlightCairngormDemo.Model
{
public class SilverPhotoModel : ModelLocator
{
private static SilverPhotoModel instance;
public static SilverPhotoModel getInstance()
{
if (instance == null)
instance = new SilverPhotoModel();
return instance;
}
private SilverPhotoModel()
{
if ( instance != null )
{
throw new CairngormError(CairngormMessageCodes.SINGLETON_EXCEPTION,
"App model (SilverPhotoModel) should be a singleton object");
}
}
private List<flickrphoto> _photoList = new List<flickrphoto>();
public List<flickrphoto> PhotoList
{
get { return _photoList; }
set { _photoList = value; NotifyPropertyChanged("PhotoList"); }
}
private int _selectedIdx = -1;
public int SelectedIdx
{
get { return _selectedIdx; }
set
{
_selectedIdx = value;
NotifyPropertyChanged("SelectedIdx");
NotifyPropertyChanged("SelectedPhotoSource");
}
}
public BitmapImage SelectedPhotoSource
{
get { return (SelectedIdx < 0 || PhotoList.Count < 2 ) ? null :
new BitmapImage(new Uri(PhotoList[SelectedIdx].ImageUrl)); }
}
private string _searchTerm = "Cairngorm";
public string SearchTerm
{
get { return _searchTerm; }
set { _searchTerm = value; }
}
}
}
6. Putting it all together
First, we need to create an instance for the SilverPhotoController
. I put the instantiation code in the Application_startup
event handler in App.xaml.cs.
private void Application_Startup(object sender, StartupEventArgs e)
{
this.RootVisual = new Page();
SilverPhotoController cntrller = SilverPhotoController.getInstance();
}
Second, we need to assign the DataContext
to be the SilverPhotoModel
. The DataContext
is set at the Loaded
event handler when the main XAML is loaded in Page.xaml.cs.
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
InitLoadEffect.Begin();
SilverPhotoModel model = SilverPhotoModel.getInstance();
LayoutRoot.DataContext = model;
}
Because of the inheritance nature of the DataContext
, we don't need to set it to our View's UserControl
s, since they'll automatically inherit the same DataContext
from the root visual in Silverlight.
Third, we need to write some data binding expression in the UserControl
's XAML markup.
PhotoSearch.xaml: Two way binding to model.SearchTerm
.
<TextBox x:Name="searchTermTextBox"
Height="30" Margin="8"
VerticalAlignment="Center"
FontSize="16"
Text="{Binding Path=SearchTerm, Mode=TwoWay}"/>
PhotoList.xaml: One way binding to model.PhotoList
, model.selectedIdx
, and FlickRPhoto.Title
for ItemTemplate
.
<ListBox x:Name="formListBox" Width="Auto" Height="Auto"
ItemsSource="{Binding Path=PhotoList}"
SelectedIndex="{Binding Path=SelectedIdx, Mode=TwoWay}"
SelectionChanged="formListBox_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Title}" Width="Auto"
Height="Auto" FontSize="12"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
ContentZone.xaml: One way binding to model.SelectedPhotoSource
.
<Image x:Name="searchResultsImage"
Source="{Binding Path=SelectedPhotoSource}"
Stretch="UniformToFill" VerticalAlignment="Center" HorizontalAlignment="Center"
Margin="8" />
Lastly and most importantly, wire-up the "Go" button Click
event with CairngormEvent
. Here is the code for the button's Click
event handler in the PhotoSearch.xaml.cs file:
private void searchBtn_Click(object sender, RoutedEventArgs e)
{
SilverPhotoModel model = SilverPhotoModel.getInstance();
if (!String.IsNullOrEmpty(model.SearchTerm))
{
CairngormEvent cgEvent =
new CairngormEvent(SilverPhotoController.SC_EVENT_SEARCH_PHOTO);
cgEvent.dispatch();
}
}
Acknowledgement
Thanks for the WPF MVC - Wrails project at CodePlex, it gave me a good starting point of porting to Silverlight 2 (Beta 2).
Also, thanks to Brad Abrams blog on the Silverlight FlickR Example, it definitely makes using the FlickR REST API easier.
History
- 2008.09.01 - First post
- 2008.09.14 - Added update notes about thread-safe changes to Silverlight Cairngorm
- 2008.10.05 - Silverlight Cairngorm v.0.0.1.2 (Downloadable source and demo project update for Thread-Safe Silverlight Cairngorm)
- Updated Silverlight Cairngorm
FrontController
---- each registered Cairngorm Event will be handled by a new instance of the corresponding Cairngorm Command, this will make Silverlight Cairngorm FrontController
work in the same way as Flex's Cairngorm 2.2.1's FrontController
. Both source and demo project can be downloaded from here.
- Also updated demo project to reflect the new
addCommand
signature to pass in the type of Command, rather than the instance of Command
- Demo project is also updated to use Silverlight 2 RC0