65.9K
CodeProject 正在变化。 阅读更多。
Home

Silverlight Cairngorm – Silverlight 4 和 Visual Studio 2010 更新

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.64/5 (4投票s)

2009年12月21日

CDDL

9分钟阅读

viewsIcon

26531

downloadIcon

617

Silverlight Cairngorm 针对 Silverlight 4 和 Visual Studio 2010 的更新。

SilverlightCairngorm_src

引言

Silverlight Cairngorm 首次发布以来,它已成功用于在 Visual Studio 2008 SP1 中开发的多个 Silverlight 2、3 项目和游戏中,作为Prism 的超轻量级替代方案。现在 Silverlight 4 Beta 和 Visual Studio 2010 Beta 2 都已可用,并且 Silverlight 4 具有丰富的新功能列表,是时候进行更新以适应新开发环境的一些新功能和反馈了。

此次更新的主题是完整性和易用性,同时利用 Silverlight 4 Beta 中提供的一些新功能。主要的新功能是添加了 ServiceLocator,新的 CairngormDelegateCairngormCommand 抽象基类,用于连接与 Web 服务交互的常见任务,以及对 CairngormEventCairngormCommand 中回调的支持。

配套的演示项目包含所有测试新功能的示例代码。

入门

示例项目的功能没有变化,它具有与以前相同的视觉效果和功能。它只是允许最终用户输入一个术语来搜索 Flickr 中匹配的图片;当搜索结果列表框中的选定项目发生更改时,相应的图片会显示在右侧窗格中。

这次更新真正改变的是利用 Silverlight Cairngorm 4 中新功能的示例项目代码:使用 ServiceLocator 注册 Flickr 服务,使用抽象的 CairngormDelegate 类来查找已注册的服务,并利用新的抽象 CairngormCommandCairngormEvent 在服务响应或错误返回时回调 View 的操作。CairngormEventCairngormCommand 中的回调支持允许 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 抽象类中与网络身份验证相关的方法实现。

图 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 中所有服务注册和查找相关的方法。

图 2. ServiceLocator 中通过 WebClient 注册和查找服务
/// <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,仅用于注册每个服务。有了框架中新提供的 ServiceLocatorCairngormDelegate 如何查找指定服务并轻松调用它?让我们来看看。

更新的 IResponder 接口和增强的抽象 CairngormDelegate

IResponder 接口由 CairngormCommand 实现,并由 CairngormDelegate 实例调用,以支持异步服务调用的简化编程模型。它是一个简单的接口,只有两个方法定义;此版本中的方法签名基于大多数 Web 服务有效负载格式为文本(XML、JSON 等)这一事实进行了更新,以及我曾经在 Flex 基于 RIA 的项目中进行的一些扩展工作。下面显示了新的接口定义

图 4 IResponder 接口
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 包含所有详细信息。

图 5. 抽象 CairngormDelegate 实现了基于字符串的 Web GET/POST 操作
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 委托

图 6. 具体 CairngormDelegate 继承自抽象基类以简化编码
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 委托易于使用,并使我们能够专注于目标服务调用的细节;无需担心那些常见的样板工作。当需要其他方法时,可以像我们为 DownloadStringUpLoadString 所做的那样轻松扩展。

遵循类似的方法,一个用于 CairngormCommand 的新抽象基类也已添加到框架中。

CairnogormCommand 的新抽象基类

Cairngorm 框架中的 Command 实际上是关于代码的分离和封装,这些代码用于调用服务,并在操作数据模型时,它会实例化正确的 cairngorm 委托来调用服务,同时还实现了 ICommandIResponder 接口。下面显示了抽象的 CairngormCommand

图 7. 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 所示。

图 8. 带有回调支持的更新的 CairngormEvent
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 中的示例。

图 9. PhotoSearch.xaml.cs 中 CairngormEvent 回调支持的示例
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 项目变得更加有趣和易于使用。

© . All rights reserved.