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

使用 Silverlight 3、.NET RIA Services 和 Azure 表存储构建三层应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (24投票s)

2009 年 7 月 9 日

CDDL

19分钟阅读

viewsIcon

171630

downloadIcon

1709

本文介绍了使用 Silverlight 3(表示层)、.NET RIA Services(业务逻辑和数据访问)以及 Windows Azure 表(数据存储)构建三层 Azure 托管应用程序的技术和注意事项。

引言

Silverlight 3、.NET RIA Services 和 Windows Azure 服务平台使得构建三层云应用程序更加容易:Silverlight 3 作为表示层,.NET RIA Services 作为业务逻辑和数据访问层,Windows Azure 表作为数据存储层。本文的示例应用程序演示了通过简单的调查应用程序的架构,所有这些技术都在 Windows Azure 中协同工作。

一些文章和帖子中已经讨论过类似的架构。MSDN 上的一篇文章(数据服务: 使用 ADO.NET 数据服务在本地或云端访问您的数据)对通过 ASP.NET MVC 在本地或云端访问 Windows Azure 表存储进行了深入讨论,但它不涉及 Silverlight 3 或 .NET RIA Services。MSDN 上关于同一问题的另一篇文章(.NET RIA Services: 使用 Silverlight 3 构建数据驱动的费用应用程序)有一个很好的示例应用程序,该应用程序利用了 Silverlight 3 和 .NET RIA Services;虽然其数据模型是基于本地数据库通过实体框架生成的 EDM(实体数据模型),但它不涉及 Azure 表存储或 Windows Azure。Nikhil Kothari 的博客文章(Nikhil Kothari's Weblog: .NET RIA Services MIX '09 Talk - Slides...)在架构方面最接近我想要实现的目标,但他的示例代码不是 Azure 项目,也没有涵盖应用程序部署到 Windows Azure 作为托管服务时的注意事项。因此,我决定写下我在构建 Azurelight 应用程序时学到的技术和注意事项:一个使用 Silverlight 3、.NET RIA Services 和 Azure 表存储构建的 Windows Azure 应用程序。

如果您安装了 Silverlight 3 RTW 插件,可以单击 此处 在 Windows Azure 中运行示例应用程序。

云中三层 RIA 简介

应用程序中的“层”是指物理上独立的二进制文件,它们促进了松耦合、可重用性和可测试性。这在概念上不同于“逻辑分层”,后者是一种管理依赖关系的逻辑代码结构。当富 Internet 应用程序需要应用程序范围的持久化数据时,通常至少有两层:表示层(由于其性质,物理上是分离的,在运行时下载并执行在客户端)和数据存储层。这里不讨论在客户端持久化数据,那是“隔离存储”,通常更多地涉及用户特定的数据。这里讨论的持久化是指应用程序范围的数据,通常存储在数据库服务器中。

中间层通常位于表示层和数据持久化层之间,它运行在服务器上,服务于客户端请求,并处理大部分应用程序业务逻辑和数据访问(CRUD)逻辑到底层数据存储。

目前,大多数面向服务的架构的 RIA 都采用此模型:一个丰富的交互式客户端通过 Web 服务与中间业务逻辑层通信。Web 服务接口的实现执行业务逻辑并处理与数据库的数据 CRUD 操作。表示层、业务逻辑层和数据持久化层是分开设计和开发的,通常使用不同的技术和平台。

这里尝试的三层 RIA 结构与上述 SOA 不同,因为 .NET RIA Services 的设计理念之一是将服务器端业务逻辑和数据访问逻辑视为表示层同一应用程序的一部分。这与松耦合的 SOA 相比是一种非常不同的思维方式;它牺牲了一些设计时间的松耦合(服务器端数据模型在设计时“投影”到客户端),但获得了生产力。

  • 应用程序数据模型,至少是 DTO(数据传输对象)或 VO(值对象),只需要在服务器项目上定义一次,然后即可自动在客户端项目中使用,从而节省了数据模型编码工作,并避免了客户端模型与服务器对象不同步。
  • 应用程序数据模型的元数据也在客户端和服务器之间“共享”;这有助于确保客户端和服务器端的数据验证逻辑相同。结合基于命名约定的代码共享,验证逻辑编写一次,执行两次,一次在客户端,一次在服务器。
  • 借助新的 Silverlight 3 数据中心控件,如 DataFormDataGridDataPager 等,投影到客户端的应用程序数据模型可以直接绑定到这些控件,控件可以生成 UI 元素(标签、字段、数据输入错误处理)。这使得快速原型开发成为可能。

第三层,数据存储,在传统的基于 SOA 的 RIA 中通常是数据库服务器。将应用程序部署到云时,ADO.NET 数据服务SQL Azure 可以帮助简化数据访问并提高可伸缩性。对于某些应用程序,数据模型只需要一些初步结构,而不要求对象之间存在关系实体,数据存储层可以绕过数据库和数据服务,利用 Windows Azure 存储。对于这类应用程序,Windows Azure 不仅是托管环境,还是可伸缩的数据存储提供商,就像文件系统在桌面应用程序中的作用一样。

例如,基于云的文件管理解决方案可以利用 Windows Azure 中的 Blob 存储作为其数据,所有数据都会持久化,但不需要数据库服务器来完成工作,因为这些非结构化数据(文件)的管理关系实体非常有限。另一个例子是基于云的通知应用程序。Windows Azure 队列可以帮助实现可靠性和可伸缩性;使用数据库可能有点大材小用。第三个例子是应用程序的数据模型很少或没有关系实体(如外键引用、多对多关系等),但实体内部具有某种结构,因此适合使用 Windows Azure 表存储。

本文中的示例应用程序属于第三类,它收集并显示来自最终用户的简单调查,用户可以指定评分并提供评论作为调查条目。调查的数据模型(Survey 类)具有结构(评分和评论等),但与其他对象没有引用关系。我们可以使用 Windows Azure 表作为其数据存储,而无需数据库。

以上是我们对应用程序架构的高层讨论。让我们看看使用 Silverlight 3、.NET RIA Service 和 Azure 表构建它的详细信息,然后将其部署到 Azure。

入门

如前所述,我们的目标是构建一个 Azure 托管的 Web 应用程序,该应用程序可以显示所有用户的反馈;每个反馈条目包含评分和评论字段,用户可以更新条目并创建新条目。在构建完应用程序后,我们将将其部署到云:Windows Azure。

完成的应用程序可以 从这里启动

示例应用程序是使用 Visual Studio 2008 SP1Windows Azure SDK (2009 年 5 月 CTP)适用于 Visual Studio SP1 的 Azure 工具 (2009 年 5 月 CTP)适用于 Visual Studio SP1 的 Silverlight 3 工具Silverlight 3 Toolkit (2009 年 7 月).NET RIA Services (2009 年 7 月预览版) 构建的。

由于 Silverlight 3 SDK 移除了 Silverlight 的 ASP.NET 控件(Silverlight 控件和 MediaPlayer 控件),需要从 MSDN 代码库下载 System.Web.Silverlight.dll。它包含在可下载的源代码中。

除了显而易见的,当使用 Azure 表构建 Azure 项目时,拥有 Windows Azure 帐户和 Windows Azure 存储帐户会很有帮助。尽管从技术上讲,我们应该能够使用本地 fabric 和开发存储(需要 SQL Server 2008 Express)来开发和调试 Azure 项目,但我在使用本地开发存储进行 Azure 表开发时遇到了很多问题,例如出现“无法连接到服务器…”、“对象不存在”等异常,或者只是静默失败——数据根本不存在,没有任何错误消息。在我创建了 Azure 存储帐户并将示例应用程序配置为使用“实际”Azure 表而不是本地模拟后,一切都顺利进行了。如果您仍然想亲自动手使用本地开发存储,SQL 脚本和数据库文件也包含在可下载的源代码包中。

示例应用程序是使用 Visual Studio 中的 Cloud Service -> Web Cloud Service(带有 Web Role 模板)创建的,并命名为 Azurelight。然后,我们需要基于 Silverlight Navigation Application 模板(注意:不是 RIA Services 安装的 Silverlight Business Application 模板)创建一个 Silverlight 3 项目(命名为 AzurelightNav),并指定 Azure 项目的 WebRole ASP.NET 项目(命名为 Azurelight_WebRole)作为其托管应用程序并链接到它。

在初始启动后,我们还需要将 Azure 类库添加到解决方案中。这是 Nikhil Kothari 的博客下载中包含的类库。它处理与 Azure 表存储的低级通信,并提供了 AzureDomainService 和基于 Azure 表的 DataContext、Entity 和 EntitySet 类型的实现。这就是为什么我们没有使用 Silverlight Business Application 模板来启动我们的 AzurelightNav Silverlight 3 应用程序,而且我们也不需要包含 Windows Azure SDK 中的 StorageClient 类库。

由于服务器端数据模型将由 .NET RIA Services 和编译器“投影”到客户端作为生成的代码,因此我们需要在解决方案的“属性”页面中将客户端项目(AzurelightNav)设置为依赖于服务器项目(Azurelight_WebRole)。否则,在定义模型后(客户端代理未生成),编译器将生成错误。

现在我们有了一个正在运行的 Azure 项目的骨架,让我们看看一些细节。

配置服务

在 Azurelight Azure 项目中,我们需要在 ServiceDefinition.csdef XML 中添加以下设置以启用 Azure 表配置设置。

<!--Fig.1 ServiceDefinition.csdef XML:-->
<ServiceDefinition name="Azurelight" 
     xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceDefinition">
  <WebRole name="WebRole" enableNativeCodeExecution="true">
    <InputEndpoints>
      <InputEndpoint name="HttpIn" protocol="http" port="80" />
    </InputEndpoints>
      <ConfigurationSettings>
          <Setting name="AccountName" />
          <Setting name="AccountSharedKey" />
          <Setting name="TableStorageEndpoint" />
      </ConfigurationSettings>
   </WebRole>
</ServiceDefinition>

服务定义中的注意事项不是新的三个配置设置,而是 WebRole 的 enableNativeCodeExecution 需要显式设置为 true。它实质上 启用了 RIA Service 在 Azure 上的完全信任模式。默认的服务定义将其设置为 false;RIA Services 在默认设置下将无法正常工作。直到 .NET RIA Services 基于 .NET 4.0 构建,我们才需要显式启用它;有关更多信息,请参阅 此处

这些新配置设置的实际值在 ServiceConfiguration.cscfg XML 文件中设置。

<!--Fig.2. ServiceConfiguration.cscfg XML: -->

<ServiceConfiguration serviceName="Azurelight" 
     xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceConfiguration">
  <Role name="WebRole">
    <Instances count="1"/>
      <ConfigurationSettings>
          <Setting name="TableStorageEndpoint" value="http://table.core.windows.net/"/> 
          <Setting name="AccountName" value="[Your_Azure_Storage_Account_Name]" />
          <Setting name="AccountSharedKey" value="[Your_Azure_Storage_Account_Key]" />
      </ConfigurationSettings>
  </Role>
</ServiceConfiguration>

正如我们在“入门”中所讨论的,我建议设置一个 Windows Azure 存储帐户来开发基于 Azure 表的项目,而不是使用本地开发存储,以避免我遇到的一些麻烦。至于设置,AccountName 不是您的 Azure Portal 帐户名,而是您在 Windows Azure 上的存储名称。AccountSharedKey 应该是 Azure 开发人员门户中存储项目摘要页面列出的主访问密钥。

帐户名和帐户共享密钥也需要在 WebRole ASP.NET 项目的 Web.config 文件中设置,以使 Azure 类库能够访问 Azure 表帐户信息。

<!--Fig.3 Azure storage account info in web.config 
    for WebRole ASP.NET Project (Azurelight_WebRole):-->

<connectionStrings>
  <add name="AzureTableConnection" 
      connectionString="name=[Your_Azure_Storage_Account_Name]; 
                        key=[ Your_Azure_Storage_Account_Key]; 
                        uri=http://table.core.windows.net; pathStyleUri=false"/>
</connectionStrings>

这里的注意事项是关于“uri=”部分:它必须始终是 http://table.core.windows.net,否则运行时将抛出异常,抱怨找不到服务。这与 Azure Portal 中关于您的表终结点 URL 不同。那个 URL 包含帐户名,例如 http://azurelight.table.core.windows.net

存储帐户名、主访问密钥和表存储终结点的缩短版本对于从您的项目访问 Azure 表存储至关重要。一旦我们配置了 Azure 项目和 WebRole ASP.NET,我们的数据存储层就准备就绪了,我们可以继续编码。

定义数据模型和 DataContext

在 WebRole 项目中定义应用程序数据模型和 DataContext 是构建我们中间层——业务逻辑层的第一步。使用 .NET RIA Services 和 Azure 表存储,我们可以采用模型中心的方法:先定义应用程序模型,然后依赖基础结构根据我们的数据模型生成 Azure 表结构,因为 Azure 表不需要预定义的固定数据架构,而数据库则需要。

由于我们的应用程序域是收集和显示用户的反馈,我们可以创建简单的 Survey 类作为我们的数据模型;Survey.cs 文件位于名为 Azurelight_WebRole/Model 的文件夹下。

//Fig.4. Data Model and MetaData
namespace Hanray.Azurelight.Model
{
    public class Survey : Entity
    {
        [Required]
        [Bindable(false)]
          [Display(AutoGenerateField=false)]
        public string Target { get; set;}
 
        [Required]
        [Range(0, 100)]
        public int? Rating { get; set; }
 
        [Required]
        public string Comments { get; set; }
    }
}

基类 Entity 是 Azure 类库中定义的抽象类。它确保所有派生类型都具有 Azure 表必需的 PartitionKeyRowKey 和时间戳属性和属性。我们的派生类型 Survey 可以专注于域模型需求,而不是底层的 Azure 表细节。为了让 Silverlight 3 的 DataForm 理解我们自动生成输入数据字段的意图,我们需要在基类 Entity(在 Azure 类项目中)中设置正确的 Display 属性。

//Fig.4.1. Display attributes added to base Entity
//to disalbe auto-generating field in DataForm
namespace Microsoft.Azure.Linq 
{
    [DataServiceKey("PartitionKey", "RowKey")]
    public abstract class Entity {

        [Key]
        [ReadOnly(true)]
        [Bindable(false)]
          [Display(AutoGenerateField=false)]
        public virtual string PartitionKey { get; set; }

        [Key]
        [ReadOnly(true)]
        [Bindable(false)]
          [Display(AutoGenerateField = false)]
          public virtual string RowKey { get; set; }

        [Timestamp]
        [ReadOnly(true)]
        [Bindable(false)]
          [Display(AutoGenerateField = false)]
          public virtual DateTime Timestamp { get; set; }
    }
}

由于 Survey 类非常简单,我们不需要使用部分类来定义其元数据;它们都定义在同一个类文件中。这些元数据将被“投影”到客户端,Silverlight 3 的 DataForm 控件将根据这些元数据生成相应的 UI 元素。此外,数据验证(评分是必需的,并且必须在指定范围内)的默认错误处理也将遵循这些元数据。

[Bindable(false)][Display(AutoGenerateField = false)] 基本上告诉 DataForm 控件不要为该属性生成输入字段;它仅用于业务逻辑,对最终用户应该是透明的。例如,Target 属性可以指向应用程序名称,然后我们的 Survey 类可以用于收集不同应用程序的反馈。此外,PartitionKeyRowKeyTimestamp 都是 Azure 表特定的字段,我们不希望在用户界面中显示它们。

在 .NET RIA Services 的上下文中,应用程序数据模型需要被包装成一个 DataContext 派生类型来适应 CRUD 操作。我们的 SurveyDataContext.cs 文件位于 Survey.cs 相同的文件夹下。

//Fig.5 SurveyDataContext:
namespace Hanray.Azurelight.Model
{
    public class SurveyDataContext : DataContext
    {
        private EntitySet<survey> _surveys;
 
        public SurveyDataContext()
            : base("AzureTableConnection")
        {
        }
 
        public virtual EntitySet<survey> Surveys
        {
            get
            {
                if (_surveys == null)
                    _surveys = new EntitySet<survey>(this, "Surveys");
 
                return _surveys;
            }
        }
    }
}

同样,基类 DataContextabstract 并在同一个 Azure 类库中定义;EntitySet 不是抽象的,它的定义在同一个库中。这是 Azure 表特定的 DataContextEntitySet

拥有数据模型和数据上下文后,我们可以定义我们的域服务类型,这是将业务逻辑公开给客户端进行操作的地方。

创建域服务

开发我们中间层的第二步是通过 DomainService 包装 DataContextDomainService 将向客户端公开数据/服务操作,并将某些操作委托给 DataContext(我们在上一节定义的 SurveyDataContext)。我们仍在 WebRole 项目中,域服务类型在 Service 文件夹下创建。

//Fig.6. SurveyService.cs:
namespace Hanray.Azurelight.Service
{
    [EnableClientAccess()]
    public class SurveyService : AzureDomainService<surveydatacontext> 
    {
        private SurveyDataContext _dataContext;
 
        public SurveyService()
        {
        }
 
        #region Azure Specifics
        protected override string GetPartitionKey() 
        {
            return "AzurelightSureveyTable";
        }
 
        protected override SurveyDataContext CreateDataContext()
        {
            if (_dataContext != null) 
            {
                return _dataContext;
            }
 
            _dataContext = base.CreateDataContext();
            _dataContext.RetryCount = 2;
 
            return _dataContext;
        }
        #endregion
 
        #region CRUD - Query, Create, Insert and Update
        //[Query]
        public IQueryable<survey> GetSurveys() 
        {
            return DataContext.Surveys;
        }
 
        //[ServiceOperation]
        public void AddSurvey(Survey survey) 
        {
            survey.RowKey = Guid.NewGuid().ToString();
            survey.Timestamp = DateTime.UtcNow;
 
            DataContext.Surveys.InsertOnSubmit(survey);
        }
 
        //[Update]
        public void UpdateSurvey(Survey currentSurvey) 
        {
            DataContext.Surveys.Attach(currentSurvey, 
                        originalSurvey, GetETag(currentSurvey));
        }
        #endregion
    }
}

DataContext 类型一样,基类 AzureDomainServiceabstract 并且来自 Azure 类库。这是一个 Azure 表存储特定的 DomainService,不支持 Azure Blob 或 Queue。重写 GetPartitionKey 方法很重要;它会影响所有要存储在 Azure 表中的新实体的 PartitionKey;它还会影响客户端查询参数。

我们的实现遵循“约定优于配置”的方法。数据操作方法名称是 GetXXXAddXXXUpdateXXX。它们将“投影”到客户端生成的代码。如果偏好配置,可以采用任意方法名称,然后应用 [Query][Create][Update][ServiceOperation] 属性。它们也将被投影到 Silverlight 3 项目。

请注意,大多数数据操作都委托给数据上下文对象。如果您的中间层需要公开特定于域的操作,您可以在此处定义并实现它们,并应用相应的 [ServiceOperation] 属性,以便它们对客户端可见。为简单起见,此示例应用程序不包含其他特定于域的操作。

AddSurvey 方法的体内,我们设置了 survey 对象的 RowKeyTimeStamp,然后调用数据上下文将其添加到列表中。注意事项是不要设置 PartitionKey,否则查询(GetSurveys)将找不到任何实体,因为客户端查询将使用 GetPartitionKey 的返回值;如果它们不同步,您将遇到我遇到的问题:保存总是成功,但所有查询在客户端都返回空列表。

由于 Survey 应用程序的性质是收集最终用户的各种反馈,我们特意省略了域服务中的 Delete 操作。最终用户只能查看、添加和更新反馈(评分和评论)条目,而不能删除。

另一个注意事项是如何实例化域服务实例。我过去常常将实例化代码放在 Global.asax.cs::Application_Start 事件处理程序中,但是启动调试时会弹出错误消息:“instances did not start within the time allowed”。基于 MSDN 上的一个 帖子,我将实例化代码移到了 Application_BeginRequest,然后错误消息就消失了。请查看 Azurelight_WebRole 项目中的 Global.asax.cs 以获取详细信息。

现在我们有了数据模型、元数据、数据上下文、域服务以及一个用于实例化它们的有效机制,编译项目将把模型和服务“投影”到客户端项目(AzurelightNav)。接下来,让我们将重点转移到表示层。

在 Silverlight 中使用投影代码

编译器将在我们的客户端 AzurelightNav 项目中创建一个名为 Generated_Code 的隐藏文件夹,并将 .NET RIA Services 投影代码(Azurelight_WebRole.g.cs)放在那里。Survey(作为数据模型)和 SurveyContext(作为与服务器交互的代理)在开发设计时即可自动用于 Silverlight 代码。

示例项目规模很小,我们实际上不需要客户端框架。但为了演示,我还是添加了 SilverlightCairngorm 作为客户端框架。它只有 26K,有助于 Silverlight 代码的关注点分离。由于投影/生成的 SurveyContext 已经处理了与服务的交互,我们不需要任何 Cairngorm Delegate 代码;所有 Cairngorm Commands 都将执行 SurveyContext 的方法。

由于我们使用的是 Silverlight Cairngorm,我们不会通过 DomainDataSource 控件在 XAML 中创建 SurveyContext 的实例,而是在 AzurelightModel 中有一个只读属性;它将作为我们视图中所有调查数据相关绑定的根数据上下文。

//Fig.7. Expose SurveyContext by read-only property in AzurelightModel:
namespace Hanray.Azurelight.Model
{
    public class AzurelightModel : ModelLocator
    {
        ……
 
        private SurveyContext _riaSvc = new SurveyContext();
        public SurveyContext RIASvc { get {    return _riaSvc; } }
 
        private Survey _feedBack;
        public Survey Feedback
        {
            get { return _feedBack;}
            set { _feedBack = value; NotifyPropertyChanged("Feedback"); }
        }
 
        public bool surveyLoaded { get; set; }

        public Survey CreateFeedback() 
        {
            return new Survey() { Target = "Azurelight Survery Demo" };
        }

        ……
    }
}

Feedback 属性是用于创建新调查条目和更新现有调查条目的数据上下文;CreateFeedback() 是一个工厂方法,用于确保所有新的 Survey 实例在应用程序中都具有一致的目标值;surveyLoaded 属性只是一个标志,指示调查列表数据是否已从服务加载。AzurelightModel 的所有其他详细信息可以在下载的源代码中的 AzurelightNav/Model/AzurelightModel.cs 中找到。

既然我们定义了模型,我们就可以创建视图并将其绑定到模型。我们的主要视图是 Feedback.xaml(在 Views 文件夹下)。它有一个 DataGrid 来显示来自 Azure 表的所有调查条目(通过 SurveyDataContext),以及三个按钮,允许用户刷新、添加或更新调查条目。

<!-- Fig.8 Feedback View XAML: -->

<StackPanel Height="Auto">
            <StackPanel HorizontalAlignment="Center" 
                   Orientation="Horizontal" Margin="10">
                <Button Content="Reload All" Width="90" 
                   Margin="10,0,0,0" IsEnabled="True" Click="btnReloadAll" />
                <Button Content="Add Entry" Width="90" 
                   Margin="10,0,0,0" IsEnabled="True" Click="btnAddClick"/>
                <Button Content="Update Entry" Width="90" 
                  Margin="10,0,0,0" 
                  IsEnabled="{Binding SelectedItem, ElementName=feedbackGrid}" 
                  Click="btnUpdateClick"/>
            </StackPanel>
            <dg:DataGrid x:Name="feedbackGrid" Width="600" Height="500" 
                         ItemsSource="{Binding Surveys}"
                         AutoGenerateColumns="False" IsReadOnly="True">
                <dg:DataGrid.Columns>
                    <dg:DataGridTextColumn Binding="{Binding Rating}" Header="Ratings"/>
                    <dg:DataGridTextColumn Binding="{Binding Comments}" Header="Comments"/>
                </dg:DataGrid.Columns>
            </dg:DataGrid>
</StackPanel>

DataGridDataContext 将在代码隐藏中设置为 AzurelightModelRIASvc 属性。

//Fig.9 Set DataGrid’s DataContext to SurveyDataContaxt instance  in the model
public partial class Feedback : Page
{
        private AzurelightModel model = AzurelightModel.Instance;
 
        public Feedback()
        {
            InitializeComponent();
 
            this.feedbackGrid.DataContext = model.RIASvc;
 
            if (model.surveyLoaded != true)
                btnReloadAll(null, null);
        }

          private void btnReloadAll(object sender, RoutedEventArgs e)
        {
            //raise event to load feedback
            CairngormEvent cgEvt = 
              new CairngormEvent(AzurelightController.SC_EVENT_LOAD_FEEDBACK);
            cgEvt.dispatch();
        }

        private void btnAddClick(object sender, RoutedEventArgs e)
        {
            model.Feedback = model.CreateFeedback();
            //Create new instance and add it to list.
            NewFeedBackPopup fbPopup = new NewFeedBackPopup(true);
            fbPopup.Show();
        }

        private void btnUpdateClick(object sender, RoutedEventArgs e)
        {
            model.Feedback = feedbackGrid.SelectedItem as Survey;
            //Create new instance and add it to list.
            NewFeedBackPopup fbPopup = new NewFeedBackPopup(false);
            fbPopup.Show();
        }
    …
}

在添加新调查条目或更新现有调查条目时,按钮的客户端事件处理程序将启动一个弹出窗口(NewFeedbackPopup.xaml)。

<!-- Fig.10. View XAML for Adding and Updating -->
<controls:ChildWindow x:Class="Hanray.Azurelight.Views.NewFeedBackPopup"
           xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
           xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
           xmlns:controls="clr-namespace:System.Windows.Controls;
                           assembly=System.Windows.Controls"
           xmlns:dataForm="clr-namespace:System.Windows.Controls;
                           assembly=System.Windows.Controls.Data.DataForm.Toolkit"
           Width="400" Height="300" 
           Title="Thanks for Your Feedback">
    <Grid x:Name="LayoutRoot" Margin="2">
        <dataForm:DataForm x:Name="feedBackForm" 
               CurrentItem="{Binding Path=Feedback}"
               AutoGenerateFields="True" 
               Header="Thanks for Your Feedback" 
               AutoEdit="True" AutoCommit="False" >
        </dataForm:DataForm>
    </Grid>
</controls:ChildWindow>

<!------------------------------- 就是这样! --------------------------->

AutoGenerateFields="True" 在 XAML 中告诉 DataForm 控件根据反馈类型(即我们在 WebRole 项目中定义的 Survey 类)生成数据录入标签和字段。其公共可绑定属性将自动生成数据绑定字段,而元数据如 [Required][Range] 将触发视觉效果(必需字段的粗体标签)和验证逻辑(底部显示错误摘要,验证失败时字段周围有红色边框)。

在代码隐藏中,它有一个用于添加和更新的工作模式,当您提交编辑且没有错误时,将引发正确的 Cairngorm 事件来添加或更新。

//Fig.11 Code behind for Adding and Updating:
namespace Hanray.Azurelight.Views
{
    public partial class NewFeedBackPopup : ChildWindow
    {
        private bool isAdding = false;
        public NewFeedBackPopup(bool isForNew)
        {
            InitializeComponent();

            this.isAdding = isForNew;

            this.feedBackForm.EditEnded += 
              new EventHandler<dataformeditendedeventargs>(feedBackForm_EditEnded);

            this.feedBackForm.DataContext = AzurelightModel.Instance;
        }

        void feedBackForm_EditEnded(object sender, DataFormEditEndedEventArgs e)
        {
            if (e.EditAction == DataFormEditAction.Commit)
            {
                //raise event to save feedback
                CairngormEvent cgEvt = new CairngormEvent(this.isAdding ? 
                  AzurelightController.SC_EVENT_POST_FEEDBACK : 
                  AzurelightController.SC_EVENT_UPDATE_FEEDBACK);
                cgEvt.dispatch();

                this.DialogResult = true;
            }
            else
                this.DialogResult = false;

            this.Close();
        }

    }
}

如上所示,所有事件处理程序的代码只是分发不同的 Cairngorm 事件;它们不直接持有和与 SurveyDataContext 交互。事件将通过 AzurelightController (AzurelightNav/Control/AzurelightController) 路由到相应的命令对象来执行实际工作。

所有命令都实现在 AzurelightNav/Command 文件夹下;它们只是简单地调用 AzurelightModelRIASvc 属性中的方法。SurveyDataContext 处理所有序列化、请求和响应事件、接收响应时的反序列化,并加载数据后更新属性。当属性更新时,Silverlight 数据绑定引擎将自动通知视图进行更新。借助 .NET RIA Services 的投影代码,表示层的管道工作大大减少和简化了。

//Fig.12 LoadSurvey Command
namespace Hanray.Azurelight.Command
{
    public class LoadFeedback : FeedbackCmdBase
    {
        private EventHandler CompletedEventHandler;
        private LoadOperation<survey> loadOp;

        public LoadFeedback()
        {
            CompletedEventHandler = new EventHandler(this.onLoadCompleted);
            loadOp = null;
        }
        
        #region ICommand Members

        public override void 
          execute(SilverlightCairngorm.Control.CairngormEvent cairngormEvent)
        {
            LoadSurveys();
        }

        #endregion

        private void LoadSurveys()
        {
            onLoadStart();

            EntityQuery allSurveysQuery = riaSvc.GetSurveysQuery();
            loadOp = riaSvc.Load(allSurveysQuery);
            loadOp.Completed += CompletedEventHandler;
        }

        private void onLoadStart()
        {
            _progressDialog = new ProgressDialog("Loading...");
            _progressDialog.Show();
        }

        private void onLoadCompleted(object sender, EventArgs e)
        {
            loadOp.Completed -= CompletedEventHandler;

            model.surveyLoaded = true;
            _progressDialog.Close();

            errorHandler(loadOp.Error);
        }
    }
}

基类 FeedbackCmdBase 定义了通用属性(模型、进度对话框、事件处理程序等),还处理 SubmitChanges(然后 PostFeedback 命令和 UpdateFeedback 命令可以共享它)。

//Fig.12 Abstract Base Command
namespace Hanray.Azurelight.Command
{
    public abstract class FeedbackCmdBase : SilverlightCairngorm.Command.ICommand
    {
        protected AzurelightModel model = AzurelightModel.Instance;
        protected SurveyContext riaSvc = AzurelightModel.Instance.RIASvc;

        protected ProgressDialog _progressDialog;

        private EventHandler CompletedEventHandler;
        private SubmitOperation submitOp;

        public FeedbackCmdBase()
        {
            CompletedEventHandler = new EventHandler(this.onCompleted);
            submitOp = null;
        }
        #region ICommand Members

        public virtual void execute(
               SilverlightCairngorm.Control.CairngormEvent cairngormEvent)
        {
        }

        protected virtual void onSubmitStart()
        {
            _progressDialog = new ProgressDialog("Saving...");
            _progressDialog.Show();
        }

        protected virtual void SubmitChanges()
        {
            onSubmitStart();
            submitOp = riaSvc.SubmitChanges();
            submitOp.Completed += CompletedEventHandler;
        }

        private void onCompleted(object sender, EventArgs e)
        {
            submitOp.Completed -= CompletedEventHandler;

            _progressDialog.Close();

            errorHandler(submitOp.Error);
        }

        protected virtual bool errorHandler(Exception Error)
        {
            if (null == Error)
                return true; //no error

            ChildWindow w = new ChildWindow()
                { Title = "Communication to Server Failed" };
            w.Content = new TextBlock() { Text = "Error:" + Error.Message };
            w.Show();

            return false; //has error
        }
        #endregion
    }
}

请查看 CommandControlModel 文件夹中的代码以获取所有详细信息。

到目前为止,我们已经拥有了一个在 Silverlight 3、.NET RIA Service、Azure 表存储下运行并在本地开发机器上运行的功能齐全的应用程序。测试完成后,右键单击 Visual Studio 中的 Azure 项目,然后选择“发布…”,部署到 Windows Azure 的过程就相当直观了。

在 Windows Azure 中托管的、使用 Silverlight 3、.NET RIA Services、Azure 表存储的完整三层应用程序可以从 http://azurelight.cloudapp.net/AzurelightNav.aspx#/Views/HomePage.xaml 启动。

总结

这是一个用于云中 RIA Services 和 Azure 表的实验性示例项目。它演示了当应用程序数据模型和元数据只定义一次,但可供服务器端和客户端代码使用时,生产力如何提高,包括自动数据录入字段生成、客户端自动错误处理,以及一次编写并在客户端和服务器上都执行的相同验证逻辑。但是,随着这些新技术的不断发展,开发人员体验可以得到改善,例如关于服务代码和客户端代码之间耦合的顾虑、在 Azure 上运行时所需的完全信任模式、Azure SDK 缺乏官方支持库、托管服务应用程序 ID 解耦等。我期待未来有更好的服务和支持。

历史

  • 2009 年 7 月 9 日 - 初始发布。
  • 2009 年 7 月 11 日 - 更新 Silverlight 3 RTW、Silverlight 3 Toolkit July 2009、.NET RIA Services July 2009 Preview。
© . All rights reserved.