Silverlight Cairngorm – Silverlight 4 和 Visual Studio 2010 更新






4.64/5 (4投票s)
Silverlight Cairngorm 针对 Silverlight 4 和 Visual Studio 2010 的更新。
引言
自Silverlight Cairngorm 首次发布以来,它已成功用于在 Visual Studio 2008 SP1 中开发的多个 Silverlight 2、3 项目和游戏中,作为Prism 的超轻量级替代方案。现在 Silverlight 4 Beta 和 Visual Studio 2010 Beta 2 都已可用,并且 Silverlight 4 具有丰富的新功能列表,是时候进行更新以适应新开发环境的一些新功能和反馈了。
此次更新的主题是完整性和易用性,同时利用 Silverlight 4 Beta 中提供的一些新功能。主要的新功能是添加了 ServiceLocator
,新的 CairngormDelegate
和 CairngormCommand
抽象基类,用于连接与 Web 服务交互的常见任务,以及对 CairngormEvent
和 CairngormCommand
中回调的支持。
配套的演示项目包含所有测试新功能的示例代码。
入门
示例项目的功能没有变化,它具有与以前相同的视觉效果和功能。它只是允许最终用户输入一个术语来搜索 Flickr 中匹配的图片;当搜索结果列表框中的选定项目发生更改时,相应的图片会显示在右侧窗格中。
这次更新真正改变的是利用 Silverlight Cairngorm 4 中新功能的示例项目代码:使用 ServiceLocator
注册 Flickr 服务,使用抽象的 CairngormDelegate
类来查找已注册的服务,并利用新的抽象 CairngormCommand
和 CairngormEvent
在服务响应或错误返回时回调 View 的操作。CairngormEvent
和 CairngormCommand
中的回调支持允许 CairngormCommand
的具体实现专注于应用程序逻辑和应用程序数据模型上的操作,无需任何代码或对 View 的引用,包括消息框。
示例项目使用Visual Studio 2010 Beta 2 构建,并且还需要适用于 Visual Studio 2010 的 Silverlight 工具才能加载项目。另外,更新的Silverlight Toolkit for Silverlight 4 将有助于您的评估工作。
当示例项目加载到 Visual Studio 中时,所有 Silverlight Cairngorm 4 的源代码都位于 SilverlightCairngorm 库项目中。现在,让我们仔细看看 Business 文件夹下的新抽象类 ServiceLocator
。
Silverlight Cairngorm 4 中的 ServiceLocator
IServiceLocator
接口和 ServiceLocator
类是在原始 Cairngorm 框架中定义的,并与 Flex RIA 项目中的服务定义 MXML 广泛使用。但是,该实现未包含在Silverlight Cairngorm 的初始版本中,主要是因为 Silverlight 2 和 3 缺乏对 WebClient
的身份验证支持。现在,随着Silverlight 4 中的网络身份验证支持,实现 IServiceLocator
接口中关于服务凭据和用户凭据的所有方法变得很方便。图 1 是 ServiceLocator
抽象类中与网络身份验证相关的方法实现。
/// <summary>
/// Set the credentials to use later.
/// </summary>
/// <param name="username"></param>
/// <param name="password"></param>
public void setCredentials(String username, String password)
{
if (String.IsNullOrEmpty(username) || String.IsNullOrEmpty(password))
return;
if (null == UserCredential)
UserCredential = new NetworkCredential();
UserCredential.UserName = username;
UserCredential.Password = password;
}
/// <summary>
/// Set the remote credentials for all registered services.
/// </summary>
/// <param name="username"></param>
/// <param name="password"></param>
public void setRemoteCredentials(String username, String password)
{
if (String.IsNullOrEmpty(username) || String.IsNullOrEmpty(password))
{
//tell all the registered service to use browser network stack
WebRequest.RegisterPrefix("http://",
System.Net.Browser.WebRequestCreator.BrowserHttp);
_netCredential = null;
}
else
{
if (null == _netCredential)
{
//tell all the registered service to use client network
//stack in order to leverage network credential introduced in SL4
WebRequest.RegisterPrefix("http://",
System.Net.Browser.WebRequestCreator.ClientHttp);
}
// NetworkCredential passing is available in ClientHttp networking stack in SL4
_netCredential = new NetworkCredential(username, password);
}
foreach (KeyValuePair<string,> oneSvePair in _httpServices)
{
var oneSve = oneSvePair.Value;
oneSve.Credentials = _netCredential;
oneSve.UseDefaultCredentials = (null != _netCredential) ? false : true;
// must be set to false if providing your own credentials
}
}
/// <summary>
/// Logs the user out of all registered services.
/// </summary>
public void logout()
{
setRemoteCredentials(null, null);
}
除了管理服务凭据外,ServiceLocator
的另一个重要职责是注册所有服务,以允许 CairngormDelegate
按名称查找指定的服务实例。在 Silverlight Cairngorm 中,我们使用 WebClient
作为服务引用。图 2 显示了 ServiceLocator
中所有服务注册和查找相关的方法。
/// <summary>
/// WebClient service dictionary
/// </summary>
private Dictionary<string,> _httpServices = new Dictionary<string,>();
/// <summary>
/// register a service
/// </summary>
/// <param name="serviceName" /><param>
/// <param name="serviceClient" /></param>
public void addHTTPService(string serviceName, WebClient serviceClient)
{
_httpServices.Add(serviceName, serviceClient);
}
/// <summary>
/// Un-register a service
/// </summary>
/// <param name="serviceName" /></param>
public void removeHTTPService(string serviceName)
{
if (_httpServices.ContainsKey(serviceName))
_httpServices.Remove(serviceName);
}
/// <summary>
/// Return the HTTPService for the given name.
/// </summary>
/// <param name="name" /></param>
/// <returns></returns>
public WebClient getHTTPService(String name)
{
if (_httpServices.ContainsKey(name))
return _httpServices[name];
return null;
}
随着 ServiceLocator
的添加,我们已经完成了 Cairngorm 框架的所有主要部分。这要归功于 Silverlight 4 的网络身份验证支持,使得这一完整性成为现实。现在我们有了完整的 ServiceLocator
,在应用程序中使用它有多么容易?图 3 显示了示例项目 (SLCairngorm2010) 中的整个类代码,您可以从中了解其易用性或困难程度。
Fig.3 Application implements concrete ServiceLocator as a singleton:
namespace SLCairngorm2010.Business
{
public class SilverPhotoService : ServiceLocator
{
public const string FLICKR_SEV_NAME = "FlickRService";
private static readonly SilverPhotoService _instance = new SilverPhotoService();
/// <summary>
/// Return the single instance of business services
/// </summary>
/// <returns></returns>
public static SilverPhotoService Instance { get { return _instance; } }
/// <summary>
/// Private constructor
/// </summary>
private SilverPhotoService()
{
//instantiate and register the HTTP service
base.addHTTPService(FLICKR_SEV_NAME, new WebClient());
}
}
}
当服务数量增加时,应用程序的构造函数中就会有更多的代码行来调用 base.addHTTPService
,仅用于注册每个服务。有了框架中新提供的 ServiceLocator
,CairngormDelegate
如何查找指定服务并轻松调用它?让我们来看看。
更新的 IResponder 接口和增强的抽象 CairngormDelegate
IResponder
接口由 CairngormCommand
实现,并由 CairngormDelegate
实例调用,以支持异步服务调用的简化编程模型。它是一个简单的接口,只有两个方法定义;此版本中的方法签名基于大多数 Web 服务有效负载格式为文本(XML、JSON 等)这一事实进行了更新,以及我曾经在 Flex 基于 RIA 的项目中进行的一些扩展工作。下面显示了新的接口定义
namespace SilverlightCairngorm.Command
{
public interface IResponder
{
void onResult(string result);
void onFault(Exception error);
}
}
有了新的接口定义,抽象的 CairngormDelegate
类可以封装所有具体 CairngormDelegate
实例所需的所有常见任务,例如调用 WebClient
上的正确方法、挂钩正确的事件、实现事件处理程序等。同样,基于大多数 Web 服务响应内容类型为文本这一事实,抽象类中实现了字符串为基础的 Web 操作的 GET 和 POST。应用程序的具体实现不需要担心调用 WebClient
的哪个方法——只要它只使用字符串在 GET 或 POST 之间进行传输。图 5 包含所有详细信息。
namespace SilverlightCairngorm.Business
{
public abstract class CairngormDelegate
{
protected IResponder responder;
/// <summary>
/// A Constructor, Recieving an IResponder instance,
/// used as "Callback" for the delegate's results -
/// through its OnResult and OnFault methods.
/// </summary>
/// <param name="responder"></param>
protected CairngormDelegate(IResponder responder)
{
this.responder = responder;
}
/// <summary>
/// subclass needs to locate the httpSvc via
/// ServiceLocator before calling any other method
/// or, exception will throw
/// </summary>
public WebClient httpSvc { get; set; }
private DownloadStringCompletedEventHandler getAsyncHandler = null;
private UploadStringCompletedEventHandler postAsyncHandler = null;
protected void getAsync(string url, bool noCache = false)
{
string randomStr = noCache ? "?noCache=" +
(new Random()).ToString() : "";
string svcUrl = url + randomStr;
if (null == getAsyncHandler)
getAsyncHandler = new
DownloadStringCompletedEventHandler(htpSvc_DownloadStringCompleted);
//didn't used the httpSvc.DownloadStringCompleted += (s, e)=>(...)
//lamda expression shortcut to enable the -= (remove event listener)
//in the result handler
httpSvc.DownloadStringCompleted += getAsyncHandler;
httpSvc.DownloadStringAsync(new Uri(svcUrl));
}
private void htpSvc_DownloadStringCompleted(object sender,
DownloadStringCompletedEventArgs e)
{
httpSvc.DownloadStringCompleted -= getAsyncHandler;
if (null != e.Error)
responder.onFault(e.Error);
else
responder.onResult(e.Result);
}
protected void postAsync(string url, string payLoad)
{
if (null == postAsyncHandler)
postAsyncHandler = new
UploadStringCompletedEventHandler(httpSvc_UploadStringCompleted);
httpSvc.UploadStringCompleted += postAsyncHandler;
httpSvc.UploadStringAsync(new Uri(url), payLoad);
}
private void httpSvc_UploadStringCompleted(object sender,
UploadStringCompletedEventArgs e)
{
httpSvc.UploadStringCompleted -= postAsyncHandler;
if (null != e.Error)
responder.onFault(e.Error);
else
//responder.onResult<string>(e.Result);
responder.onResult(e.Result);
}
}
}
现在,让我们回到关于 Cairngorm 委托如何从 ServiceLocator
查找服务,以及更重要的是,如何轻松地让一个具体的 CairngormDelegate
类做到这一点?当一个具体的 Cairngorm 委托继承自上面的抽象基类时,一切都非常简单。图 6 是示例应用程序中实现的 SearchPhoto
委托
namespace SLCairngorm2010.Business
{
public class SearchPhotoDelegate : CairngormDelegate
{
public SearchPhotoDelegate(IResponder responder)
: base(responder)
{
base.httpSvc = SilverPhotoService.Instance.getHTTPService(
SilverPhotoService.FLICKR_SEV_NAME);
}
public void SendRequest(string searchTerm)
{
string apiKey = "[Your FlickR API Key]";
string url = String.Format("http://api.flickr.com/services/" +
"rest/?method=flickr.photos.search&api_key={1}&text={0}",
searchTerm, apiKey);
base.getAsync(url);
}
}
}
在示例项目中,指定的 Flickr 服务在 SilverPhotoServiceLocator
中注册,并通过传递服务名称调用 getHTTPService
来查找实际的 WebClient
实例。 base.getAsync
挂钩了事件处理程序和 IResponder
方法与指定的 WebClient
实例,然后发送 HTTP 请求进行搜索。当响应返回或发生错误时,会在 SearchPhotoCommand
中调用实现的 IResponder
方法。
抽象 CairngormDelegate
中的代码确实使具体的 Cairngorm 委托易于使用,并使我们能够专注于目标服务调用的细节;无需担心那些常见的样板工作。当需要其他方法时,可以像我们为 DownloadString
和 UpLoadString
所做的那样轻松扩展。
遵循类似的方法,一个用于 CairngormCommand
的新抽象基类也已添加到框架中。
CairnogormCommand 的新抽象基类
Cairngorm 框架中的 Command 实际上是关于代码的分离和封装,这些代码用于调用服务,并在操作数据模型时,它会实例化正确的 cairngorm 委托来调用服务,同时还实现了 ICommand
和 IResponder
接口。下面显示了抽象的 CairngormCommand
类
namespace SilverlightCairngorm.Command
{
public abstract class CairngormCommand : ICommand, IResponder
{
protected CairngormEvent cgEvt = null;
#region ICommand Members
virtual public void execute(CairngormEvent cairngormEvent)
{
//derived class call base first before call their own
cgEvt = cairngormEvent;
}
#endregion
#region IResponder Members
virtual public void onResult(string result)
{
//derived class call their own logic first before call this
//only needs to call base when callBack is needed
if (null != cgEvt)
cgEvt.responderCallBack(true);
}
virtual public void onFault(Exception error)
{
//derived class call their own logic first before call this
//only needs to call base when callBack is needed
if (null != cgEvt)
cgEvt.onFaultCallBack(error);
}
#endregion
}
}
事实上,这个基类中执行的“常见任务”并不多;它只是为相应的 CairngormEvent
创建了一个缓存,并调用缓存引用上的方法。这个抽象类的真正目的是挂钩相应的 .NET 委托作为回调,该回调在 cairngorm 事件中引用。派生类可以利用它来支持从 Command 回调到 View,因此 Cairngorm Command 代码可以完全独立于 View。
Cairngorm 事件和 Command 中的 View 回调支持
根据 Cairngorm 框架的设计,View 处理所有视觉效果,用户手势(鼠标点击、按键等)通常会导致引发应用程序定义的 Cairngorm 事件。Cairngorm Controller 通过实例化相应的注册 Cairngorm Command 来处理事件,该 Command 通过 ICommand
接口调用 Execute
方法。然后,Cairngorm Command 使用正确的 Cairngorm 委托异步调用服务。当服务响应返回或发生错误时,Cairngorm 委托通过 IResponder
接口回调 Command。当 View 根据不同的服务响应或模型状态更改进行更新时,Command 不会回调 View;这原本是打算通过数据绑定来处理的。
在我几乎所有的 RIA 工作经验中,我发现 Command 更改模型后,数据绑定引擎可以自动处理大部分 View 更改,但在某些情况下,这效率不高——我们需要 Command 回调 View 来执行一些超出正常数据绑定范围的操作。例如,在提交弹出窗口中的表单数据时,弹出窗口可能有一个视觉指示器来显示应用程序正在与服务器通信。当服务器响应正常时,我们会希望停止视觉指示器并关闭弹出窗口,然后 View 的导航可能会随之进行。为了避免将 View 的代码混入 Command(在 Command 中关闭弹出窗口),我们希望所有与视觉相关的代码都保留在 View 中,并让 Command 通过 .NET 委托回调 View 来执行预期的 View 操作。这种需求是为了通过事件支持从 Command 回调到 View。
Cairngorm Command 中的此回调支持不仅将 Command 与 View 解耦,满足了应用程序架构中的“关注点分离”原则,还使得 Command 的单元测试成为可能,并使应用程序结构中的测试驱动开发成为可能。
我们在图 7 的抽象 CairngormCommand
类中已经看到了回调的挂钩。支持回调的更新的 CairngormEvent
类如图 8 所示。
namespace SilverlightCairngorm.Control
{
/// <summary>
/// A parent Class for all Cairngorm events.
/// </summary>
public class CairngormEvent
{
/// <summary>
/// Constructor of a cairngorm event
/// </summary>
/// <param name="typeName"></param>
public CairngormEvent( String typeName)
{
this.Name = typeName;
}
/// <summary>
/// every event must have a unique name - multiple, similar, events
/// may be grouped in one class - as long as the "Name" property
/// changes from one event to the other.
/// </summary>
public string Name { get; set; }
/// <summary>
/// The data property can be used to hold information to be passed with the event
/// in cases where the developer does not want to extend the CairngormEvent class.
/// However, it is recommended that specific classes are created for each type
/// of event to be dispatched.
/// </summary>
public object Data { get; set; }
/// <summary>
/// Callback delegate usually used when gets response
/// while comand needs to callback to View
/// Optional field
/// </summary>
public Action<bool> responderCallBack = delegate {};
/// <summary>
/// Callback delegate usually used when command needs
/// to callback to View's method to handle exception
/// Optional field
/// </summary>
public Action<exception> onFaultCallBack = delegate {};
/// <summary>
/// Helper funtion to raise event without requiring
/// client code to call CairngormEventDispatcher.getInstance()
/// </summary>
public void dispatch()
{
CairngormEventDispatcher.Instance.dispatchEvent(this);
}
}
}
定义了两个回调 .NET 委托;如果它们不在具体的 Cairngorm 事件中定义,Command 将调用默认的空委托,以避免在调用之前进行空检查。
要使用它,应用程序 View 的代码只需定义具有正确签名的回调函数,然后在分派它之前将其设置为相应的回调属性;请参见图 9 中的示例。
namespace SLCairngorm2010.View
{
public partial class PhotoSearch : UserControl
{
protected ProgressDialog _progressDialog;
public PhotoSearch()
{
InitializeComponent();
}
private void searchBtn_Click(object sender, RoutedEventArgs e)
{
SilverPhotoModel model = SilverPhotoModel.Instance;
if (!String.IsNullOrEmpty(model.SearchTerm))
{
onSearchingStart();
CairngormEvent cgEvent = new
CairngormEvent(SilverPhotoController.SC_EVENT_SEARCH_PHOTO);
cgEvent.responderCallBack = onSearchCompleted;
cgEvent.onFaultCallBack = onSearchError;
cgEvent.dispatch();
}
}
protected virtual void onSearchingStart()
{
_progressDialog = new ProgressDialog("Searching Flickr...");
_progressDialog.Show();
}
private void onSearchCompleted(bool result)
{
if (null != _progressDialog)
_progressDialog.Close();
_progressDialog = null;
}
private void onSearchError(Exception Error)
{
if (null == Error)
return; //no error
if (String.IsNullOrEmpty(Error.Message) ||
String.IsNullOrWhiteSpace(Error.Message))
return; //nothing to show
ChildWindow w =
new ChildWindow() { Title = "Communication to Server Failed" };
w.Content = new TextBlock() { Text = "Error:" + Error.Message };
w.Show();
}
}
}
总结
Silverlight Cairngorm 的这次更新完成了向框架添加易于使用的 ServiceLocator
,并改进了 IResponder
接口和抽象基类 CairngormDelegate
,使得调用作为内容类型的字符串服务变得更加容易。此外,带有扩展 CairngormEvent
的抽象 CairngormCommand
类通过 .NET 委托支持从 Command 回调到 View。所有这些更改和扩展都是在 Visual Studio 2010 Beta 2 的 Silverlight 4 Beta 中开发和测试的。希望这次更新能帮助您的下一个 Silverlight 4 项目变得更加有趣和易于使用。