Silverlight Cairngorm - 将 Cairngorm 移植到 .NET





5.00/5 (11投票s)
将 Cairngorm 2.2.1 移植到 Silverlight 2 Beta 2。包含所有源代码和示例应用程序。

引言
目前,Silverlight 没有官方的 MVC/MVP/MV-VM 框架。在开发企业应用程序或在 Silverlight 中构建大型 LOB 应用程序时,客户端架构对于“开发可扩展性”变得很重要。尽管有一些适用于 WPF 的指南或框架,但没有一个可以轻松应用于 Silverlight。Adobe 的Cairngorm自 2006 年以来已广泛用于 Flex RIA 应用程序;它具有易于理解的概念、公认的设计模式,并且已证明能很好地扩展大型业务线应用程序的开发。本文描述了将 Cairngorm 移植到 Silverlight (Beta 2) 的努力,使用了 Visual Studio 2008 SP1,详细介绍了哪些概念/类已被采用,哪些已被放弃,并提供了一个示例应用程序来演示其工作原理和预期用途。它极大地帮助我在工作中为一个潜在的大型面向消费者的金融应用程序创建了一个 Silverlight 原型;希望这项工作能对其他 Silverlight 开发人员有所帮助。
Cairngorm 简介
Cairngorm 是 Adobe Consulting 开发的、使用 Flex 或 AIR 构建的富互联网应用程序的轻量级微架构。其目标应用程序是企业 RIA 或中大型 LOB RIA。正如Introducing Cairngorm 文档中所述,Cairngorm 的主要优势是
- 添加新功能或更改现有功能更加容易:可以通过添加新的 View、Model、Event、Command 和 Delegate(注意:不是 .NET Delegate,而是指 Cairngorm Delegate)来“插入”新功能,而不会更改/影响其他功能
- 支持敏捷团队开发流程:设计人员(Views)、前端开发人员(Model、Events、Commands、Delegates、Data Binding)和数据服务开发人员(Web Services)可以并行工作
- 易于维护和调试,并且更容易单元测试业务逻辑代码。
Cairngorm 帮助开发人员根据其角色/职责识别、组织和分离代码;最高级别上,它具有以下主要组件
- Model 包含数据对象和通过 `ModelLocator` 存储的数据状态
- Controller 处理 Cairngorm 事件并通过 `FrontController` 执行相应的 `Command` 类
- Commands 是非 UI 组件,用于处理业务逻辑,通常实现 `ICommand` 和 `IResponder` 接口
- Events 是自定义事件,用于触发业务对象(即 Commands)开始处理,通常由 View 的事件(应用程序事件、用户输入事件等)处理程序引发
- `ServiceLocator` 是预配置的客户端/服务器通信组件的存储库
- Cairngorm Delegates 是了解如何与 Web Services 通信并将 Result 和 Fault 事件路由到 Command 的类,通过 `IResponder` 接口
- Views 渲染 Model 的数据,并使用 Events 与 Controller 通信,还通过 Data Binding 监视 Model 数据变化
有关 Cairngorm 的更多信息,请参阅Cairngorm Developer Documentation。
Silverlight Cairngorm 的变化
在为 Silverlight 实现 Cairngorm 时,所有主要概念/组件都得到了保留,除了 `ServiceLocator`。详情如下
1. 无需 ServiceLocator,直接在 Delegate 中使用 WebClient
由于在 Silverlight 中,`WebClient` 类和生成的服务代理以异步和强类型的方式用于与服务交互,因此 Cairngorm Delegate 对象实例化 `WebClient` 或生成的服务代理并直接使用它们更加方便,消除了对 `ServiceLocator` 的需求。
但是,在源代码中,我仍然在 _Business_ 子文件夹中包含 `IServiceLocator` 接口的 C# 定义和一个 `ServiceLocator` 的抽象 C# 类——如果您需要它们的话。
[更新说明 2008/9/14] 线程安全的 Silverlight Cairngorm v0.0.0.1 实现已从源代码中移除了 `ServiceLocator` 和 `IServiceLocator` 接口,可以在另一篇文章中下载。
2. ModelLocator 变为抽象类并实现 INotifyPropertyChanged
在抽象 `ModelLocator` 中实现 `INotifyPropertyChanged` 接口,使得派生的应用程序 `ModelLocator`(在示例应用程序中,它是 `SilverPhotoModel` 类)可以实现该接口,并确保派生的 Model 可以“绑定”到 View (XAML)。我非常希望 Silverlight 的未来版本能提供类似 Flex ActionScript 代码中的 `[Bindable]` 属性,这样开发人员就不需要关心 `INotifyPropertyChanged` 接口,并在所有 setter 方法中调用 `NotifyPropertyChanged("PropertyName")`,这项工作更适合编译器。这样,设计 Model 并使其可绑定会容易得多。
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
}
应用程序 Model 类将继承自上述 `ModelLocator`。派生类将实现 Singleton 模式,并且还需要在 `public` 可绑定属性的 setter 方法中调用 `NotifyPropertyChanged`。您将在示例应用程序中看到一个例子。
[更新说明 2008/9/14] `ModelLocator` 和 `INotifyPropertyChange` 接口的线程安全实现在另一篇文章中。
3. FrontController 变为抽象类,CairngormEventDispatcher 变为内部类
将 `CairngormEventDispatcher` 设为 `internal` 实际上简化了应用程序引发 Cairngorm 事件的代码:强制其不访问 `CairngormEventDispatcher`;相反,只需实例化一个 `CairngormEvent` 对象,然后调用其 `Dispatch` 方法。`CairngormEventDispatcher` 只在 Cairngorm 程序集中是内部的,应用程序不需要关心它。
namespace SilverlightCairngorm.Control
{
/// <summary>
/// Used to dispatch system events, by raising an event that the
/// controller class subscribes to every time any system event is
/// raised.
/// Client code has no need to use this class. (internal class)
/// </summary>
internal class CairngormEventDispatcher
{
private static CairngormEventDispatcher instance;
/// <summary>
/// Returns the single instance of the dispatcher
/// </summary>
/// <returns>single instance of the dispatcher</returns>
public static CairngormEventDispatcher getInstance()
{
if ( instance == null )
instance = new CairngormEventDispatcher();
return instance;
}
/// <summary>
/// private constructor
/// </summary>
private CairngormEventDispatcher()
{
}
/// <summary>
/// The subscriber to a system event must accept as argument
/// the CairngormEvent raised (within a CairngormEventArgs\
/// object)
/// </summary>
public delegate void EventDispatchDelegate
(object sender, CairngormEventArgs args);
/// <summary>
/// The single event raised whenever a Cairngorm system event occurs
/// </summary>
public event EventDispatchDelegate EventDispatched;
/// <summary>
/// dispatchEvent raises a normal .net event, containing the
/// instance of the CairngormEvent raised - to be handled by
/// the Controller Class
/// </summary>
public void dispatchEvent(CairngormEvent cairngormEvent)
{
if (EventDispatched != null)
{
CairngormEventArgs args = new CairngormEventArgs(cairngormEvent);
EventDispatched(null, args);
}
}
}
}
Cairngorm `FrontController` 在 Silverlight Cairngorm 中得到保留
namespace SilverlightCairngorm.Control
{
/// <summary>
/// The system controller's parent, implementing the event-command
/// relationship inner-workings, using a dictionary relating
/// event names to commands.
///
/// subscribes to the EventDispatched event of the CairngormEventDispatcher
/// to handle all system events.
/// </summary>
public abstract class FrontController
{
/// <summary>
/// The dictionary of eventNames and corresponding commands to be executed
/// </summary>
private Dictionary eventMap = new Dictionary();
public FrontController()
{
CairngormEventDispatcher.getInstance().EventDispatched +=
new CairngormEventDispatcher.EventDispatchDelegate(ExecuteCommand);
}
/// <summary>
/// Whenever the CairngormEventDispatcher raises an event, this
/// method gets the CairngormEvent inside it - and calls
/// the execute() method on the corresponding ICommand
/// </summary>
void ExecuteCommand(object sender, CairngormEventArgs args)
{
if (eventMap.ContainsKey(args.raisedEvent.Name))
{
eventMap[args.raisedEvent.Name].execute(args.raisedEvent);
}
}
/// <summary>
/// register a Cairngorm event to FrontController
/// </summary>
public void addCommand(string cairngormEventName, ICommand command)
{
eventMap.Add(cairngormEventName, command);
}
}
}
4. 添加了新的抽象类:CairngormDelegate
所有 `CairngormDelegate` 类都将从此类派生;它要求所有派生类都包含对 `IResponder` 的引用。
namespace SilverlightCairngorm.Business
{
public abstract class CairngormDelegate
{
protected IResponder responder;
/// <summary>
/// A Constructor, Receiving an IResponder instance,
/// used as "Callback" for the delegate's results -
/// through its OnResult and OnFault methods.
/// </summary>
protected CairngormDelegate(IResponder responder)
{
this.responder = responder;
}
}
}
5. 从 SilverlightCairngorm 中移除的类/接口
已移除 `IValueObject` 接口和 `ValueObject` 类型。因为,数据传输对象很可能会被生成。
`ViewHelper` 和 `ViewLocator` 也不在 `SilverlightCairngorm` 中,因为在数据绑定场景下让模型直接访问视图不是一个好主意;
`SequenceCommand` 未移植,在链接事件/命令的情况下,可以从 Command 中的 `onResult` 方法引发 Cairngorm 事件。
`HTTPService`、`WebService` 和 `RemoteObject` 都是 Flex 特有的,不包含在 `SilverlightCairngorm` 中。
Silverlight Cairngorm 示例应用程序
本示例应用程序演示了如何在 Silverlight 应用程序中使用 Cairngorm。如果您安装了 Silverlight 2 Beta 2,可以从这里运行该应用程序。它非常简单:允许用户输入一个词来使用 FlickR REST API 搜索照片,将搜索结果绑定到左侧的导航列表,然后自动在右侧显示第一张图片。当然,当用户在列表中选择一张图片时,显示图片会更新。
1. 在 XAML 中定义 View
自动生成的 _Page.xaml 已扩展以包含应用程序布局,它有一个简单的文本动画来显示应用程序标题。它还在其布局标记中引用了三个 `UserControl`。思路是 _Page.xaml 只提供一个入口点和 View 的应用程序布局。实际的功能 Views 由 `UserControl` 定义,以适应设计人员可能进行的 UI 设计更改。
所有 `UserControl` 都定义在 _View_ 子文件夹中,并且具有 `SilverlightCairngormDemo.View` 作为它们的命名空间。_PhotoSearch.xaml_ 只有一个用于用户输入搜索词的文本框和一个“Go”按钮来触发搜索照片操作。_PhotoList.xaml_ 有一个 `ListBox` 和一个 `ItemTemplate` 来将搜索结果渲染为文本(照片标题)列表。`PhotoSearch` 和 `PhotoList` 在 _Page.xaml_ 中垂直堆叠,作为左侧导航列表。
_ContentZone.xaml_ 只包含一个 `Image` 控件来显示选定的照片。
2. 定义 SearchPhotoDelegate
让我们从底向上开始使用 `SilverlightCairngorm`。通过派生自 `CairngormDelegate`,`SearchPhotoDelegate` 类是唯一了解如何与 FlickR REST API 通信的类——发送请求并将 `onResult` 和 `onFault` 调用路由到 `IResponsder`
namespace SilverlightCairngormDemo.Business
{
public class SearchPhotoDelegate : CairngormDelegate
{
public SearchPhotoDelegate(IResponder responder)
: base(responder)
{
}
public void SendRequest(string searchTerm)
{
// SilverPhotoService svcLocator = SilverPhotoService.getInstance();
// WebClient flickrService =
// svcLocator.getHTTPService(SilverPhotoService.FLICKR_SEV_NAME);
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. 定义 SearchPhotoCommand
`SearchPhotoCommand` 实现 `IResponder` 和 `ICommand` 接口。当 Controller 处理 Cairngorm 事件时,将调用 `ICommand.execute`;当没有发生异常时,`SearchPhotoDelegate` 将调用 `IResponder.onResult`。`IResponder.onFault` 处理来自 `SearchPhotoDelegate` 的异常和错误或无效数据。
namespace SilverlightCairngormDemo.Command
{
public class SearchPhotoCommand : ICommand, IResponder
{
private SilverPhotoModel model = SilverPhotoModel.getInstance();
#region ICommand Members
public void execute(CairngormEvent cairngormEvent)
{
//get search term from model
string toSearch = model.SearchTerm;
//begin talk to web service
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;
}
//update the photoList data in model
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; //display the 1st image
else
onFault("No such image, please search again.");
}
public void onFault(string errorMessage)
{
//display the error message in PhotoList
model.SelectedIdx = -1;
model.PhotoList = new List<flickrphoto />()
{ new FlickRPhoto() { Title = errorMessage } };
}
#endregion
}
}
[更新说明 2008/9/14] 另一篇文章这里提供了更新的 `SearchPhotoCommand`,它使用线程池线程执行 XML 解析和数据转换到对象集合,以测试 `ModelLocator` 的线程安全性。
4. 定义 SilverPhotoController
`SilverPhotoController` 派生自 `FrontController`,将特定事件名称与 `SearchPhotoCommand` 注册,并在运行时将事件路由到相应的 Command。
namespace SilverlightCairngormDemo.Control
{
public class SilverPhotoController : FrontController
{
public const string SC_EVENT_SEARCH_PHOTO = "cgEvent_SearchPhoto";
private static SilverPhotoController instance;
/// <summary>
/// Returns the single instance of the controller
/// </summary>
/// <returns>single instance of the dispatcher</returns>
public static SilverPhotoController getInstance()
{
if ( instance == null )
instance = new SilverPhotoController();
return instance;
}
/// <summary>
/// private constructor
/// </summary>
private SilverPhotoController()
{
base.addCommand(SC_EVENT_SEARCH_PHOTO, new SearchPhotoCommand());
}
}
}
5. 定义 Model
Model 中的第一个类型是 `FlickRPhoto` 类;它代表从搜索返回的照片数据对象。
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);
}
}
}
}
现在,是时候定义 `SilverPhotoModel` 了;它派生自 `ModelLocator` 并实现为 Singleton。请注意 `NotifyPropertyChanged("PropertyName")` 调用,这对数据绑定至关重要。
namespace SilverlightCairngormDemo.Model
{
public class SilverPhotoModel : ModelLocator
{
private static SilverPhotoModel instance;
/// <summary>
/// Returns the single instance of the app model
/// </summary>
/// <returns>single instance of the app model</returns>
public static SilverPhotoModel getInstance()
{
if (instance == null)
instance = new SilverPhotoModel();
return instance;
}
/// <summary>
/// Private constructor for singleton object
/// </summary>
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>();
/// <summary>
/// Data model for search result data binding
/// </summary>
public List<flickrphoto> PhotoList
{
get { return _photoList; }
set { _photoList = value; NotifyPropertyChanged("PhotoList"); }
}
private int _selectedIdx = -1;
/// <summary>
/// Data model for selected photo index
/// </summary>
public int SelectedIdx
{
get { return _selectedIdx; }
set
{
_selectedIdx = value;
NotifyPropertyChanged("SelectedIdx");
NotifyPropertyChanged("SelectedPhotoSource");
}
}
/// <summary>
/// Read-only property for image displaying
/// </summary>
public BitmapImage SelectedPhotoSource
{
get { return (SelectedIdx < 0 || PhotoList.Count < 2 ) ? null :
new BitmapImage(new Uri(PhotoList[SelectedIdx].ImageUrl)); }
}
private string _searchTerm = "Cairngorm";
/// <summary>
/// Data model for search term
/// </summary>
public string SearchTerm
{
get { return _searchTerm; }
set { _searchTerm = value; }
}
}
}
6. 将它们组合在一起
首先,我们需要创建一个 `SilverPhotoController` 的实例。我将实例化代码放在 _App.xaml.cs_ 中的 `Application_startup` 事件处理程序中。
private void Application_Startup(object sender, StartupEventArgs e)
{
this.RootVisual = new Page();
//create Cairngorm controller instance
SilverPhotoController cntrller = SilverPhotoController.getInstance();
}
其次,我们需要将 `DataContext` 设置为 `SilverPhotoModel`。`DataContext` 在主 XAML 在 _Page.xaml.cs_ 中加载时,在 `Loaded` 事件处理程序中设置。
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
InitLoadEffect.Begin();
//create Cairngorm model instance
SilverPhotoModel model = SilverPhotoModel.getInstance();
//bind model to view
LayoutRoot.DataContext = model;
}
由于 `DataContext` 的继承特性,我们无需将其设置给 View 的 `UserControl`,因为它们会自动从 Silverlight 的根视觉元素继承相同的 `DataContext`。
第三,我们需要在 `UserControl` 的 XAML 标记中编写一些数据绑定表达式。
_PhotoSearch.xaml_:双向绑定到 `model.SearchTerm`。
<TextBox x:Name="searchTermTextBox"
Height="30" Margin="8"
VerticalAlignment="Center"
FontSize="16"
Text="{Binding Path=SearchTerm, Mode=TwoWay}"/>
_PhotoList.xaml_:单向绑定到 `model.PhotoList`、`model.selectedIdx` 以及 `ItemTemplate` 的 `FlickRPhoto.Title`。
<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_:单向绑定到 `model.SelectedPhotoSource`。
<Image x:Name="searchResultsImage"
Source="{Binding Path=SelectedPhotoSource}"
Stretch="UniformToFill" VerticalAlignment="Center" HorizontalAlignment="Center"
Margin="8" />
最后,也是最重要的,将“Go”按钮的 `Click` 事件与 `CairngormEvent` 连接起来。这是 _PhotoSearch.xaml.cs_ 文件中按钮 `Click` 事件处理程序的代码
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();
}
}
致谢
感谢 CodePlex 上的 WPF MVC - Wrails 项目,它为我移植到 Silverlight 2 (Beta 2) 提供了一个很好的起点。
同样,感谢 Brad Abrams 关于 Silverlight FlickR 示例的博客,这使得使用 FlickR REST API 更加容易。
历史
- 2008.09.01 - 首次发布
- 2008.09.14 - 添加了关于Silverlight Cairngorm 线程安全更改的更新说明
- 2008.10.05 - Silverlight Cairngorm v.0.0.1.2(可下载源代码和演示项目更新,用于线程安全 Silverlight Cairngorm)
- 更新了 Silverlight Cairngorm `FrontController`——每个注册的 Cairngorm 事件将由相应 Cairngorm Command 的新实例处理,这将使 Silverlight Cairngorm `FrontController` 的工作方式与 Flex 的 Cairngorm 2.2.1 的 `FrontController` 相同。源代码和演示项目都可以从这里下载。
- 还更新了演示项目,以反映新的 `addCommand` 签名,该签名传递 Command 的类型而不是 Command 的实例。
- 演示项目也已更新为使用 Silverlight 2 RC0