Introduction
Silverlight 3, .NET RIA Services, and Windows Azure Services Platform makes a 3-tier Cloud application easier to build: Silverlight 3 as presentation tier, .NET RIA services as the business logic and data access tier, and Windows Azure Table as the data storage tier. The sample application in this article demonstrates the architecture with a simple Survey application with all these technologies working together from Windows Azure.
A similar architecture has been discussed in some articles and posts. One article in MSDN (Data Services: Access Your Data On Premise Or In The Cloud With ADO.NET Data Services) has great discussions about accessing Windows Azure Table storage either on premise or in the Cloud by ASP.NET MVC, but it doesn’t involve Silverlight 3 or .NET RIA Services. Another article on the same issue on MSDN (.NET RIA Services: Building A Data-Driven Expense App with Silverlight 3), has a great sample application that leverages Silverlight 3 and .NET RIA Services; while its data model is EDM (Entity Data Model) generated from Entity Framework based on an on-premise database, it doesn’t involves Azure Table storage or Windows Azure. A blog post from Nikhil Kothari (Nikhil Kothari's Weblog: .NET RIA Services MIX '09 Talk - Slides...) is the closest to what I’m trying to achieve in terms of architecture, but his sample code is not an Azure project, and doesn’t cover the caveats when the application is put on Windows Azure as a hosted service. So, I decided to write down the techniques and caveats I learned when I built the Azurelight application: a Windows Azure application built using Silverlight 3, .NET RIA Services, and Azure Table storage.
If you have the Silverlight 3 RTW plug-in installed, you can click here to run the sample application from Windows Azure.
Brief on 3-Tier RIA in the Cloud
A tier in an application implies physically self-contained binaries that promote loose-couplings, reusability, and testability. It’s conceptually different with layers, which is a logical code structure that manages dependencies. When a Rich Internet Application needs application-wide persisted data, it usually has at least two tiers: the presentation tier is physically separated by nature (downloaded and executed in the client at runtime) and a data storage tier. We are not discussing persisting data in the client, that’s Isolated Storage, which usually is more about user-specific data. The persistence discussed here is about application-wide data that usually is stored in a database server.
A middle tier usually sits between the presentation tier and the data persistence tier, it runs on the server, serves the client request, and handles most application business logic and data access (CRUD) logic to the underlining data store.
Currently, most service-oriented architected RIAs are designed in this model: a rich interactive client talks with a middle business logic tier through Web Services. The implementation of the Web Service interface performs the business logic and handles data CRUD operations to and from the database. Presentation, Business Logic and Data Persistence tiers are separately designed and developed, often in different technologies and platforms.
The 3-tier RIA structure experimented here is different from the above SOA, since one of the design philosophies of .NET RIA Services is to consider the server side business logic and data access logic are part of the same application as the presentation tier. This is a very different thinking comparing to loose-coupled SOA; it sacrifices some design time loose coupling (server side data model is “projected” to the client at design time), but gains productivities:
- Application data model, at least DTOs (Data Transfer Objects) or VOs (Value Objects), just need to be defined once at the server project and are then available at the client project automatically, saving data model coding efforts and avoiding the client side model being out of sync with the server objects.
- The metadata of the application data model is also “shared” between the client and the server; it helps to make sure the client side and the server side data validation logic are the same. Together with the naming convention based code sharing, validation logic is written once and executed twice, once in the client and ocne in the server.
- With the help of the new Silverlight 3 data-centric controls,
DataForm
, DataGrid
, DataPager
, etc., the projected client side application data model can be directly bound to these controls, and the controls can generate UI elements (labels, fields, data entry error handing). This could make quick prototypes becomes possible.
The third tier, data storage, is often a database server in traditional SOA based RIAs. When putting the application to Cloud, ADO.NET Data Services and SQL Azure can help to make data access easier and scalable. For some applications, the data model only needs some preliminary structure, while not demanding relational entities between objects, the data storage layer can bypass the database and data service to utilize Windows Azure Storage. For this type of applications, Window Azure is not only the hosting environment, but also a scalable data storage provider, like the file system's role in desktop applications.
For example, a Cloud based file management solution can leverage Blob Storage in Windows Azure for its data, all data getting persisted, but not requiring a database server to do the work, since those unstructured data (files) has very limited relational entities to manage. Another example is a Cloud based Notification application. Windows Azure Queue can help with the reliability and scalability; using a database might be overkill. A third example is that an application’s data model has very few or no relational entities (like foreign key references, many to many relationships, etc.), but has some sort of structure within the entity, and so it’s suitable to use Window Azure Table Storage.
The sample application in this article fits into the third category, it collects and displays a simple survey from the end user, and the user can specify a rating and give comments as a survey entry. The data model for survey (the Survey
class) has a structure (rating and comments, etc.) while doesn’t have referencing relations with other objects. We can use Windows Azure Table as its data store, and no database is needed.
What we saw above is a high level discussion about the application architecture. Let’s look at some details about how to build it with Silverlight 3, .NET RIA Service, and Azure Table, and then we’ll deploy it to Azure.
Getting Started
As mentioned above, our goal is to build an Azure hosted web application that can display feedback from all users; each feedback entry contains rating and comments fields, and the user can update an entry and also create new entries. In case other users add new entries, the application should provide a refresh function to reload all the persisted feedback entries. After we build the application, we’ll deploy it to Cloud: Windows Azure.
The completed application can be launched from here.
The sample application is built with Visual Studio 2008 SP1, with Windows Azure SDK (May 2009 CTP), Azure Tools for Visual Studio SP1 (May 2009 CTP), Silverlight 3 Tools for Visual Studio SP1, Silverlight 3 Toolkit July 2009, and .NET RIA Services (Jul 2009 Preview).
Because the Silverlight 3 SDK removed the ASP.NET Controls for Silverlight (Silverlight control and MediaPlayer control), the System.Web.Silverlight.dll needs to be downloaded from MSDN Code Gallery. It's included in the downloadable source code.
Beyond the obvious, it’s helpful to have a Windows Azure account and a Windows Azure Storage account set up when building an Azure project with Azure Table. Although technically, we should be able to develop and debug an Azure project with a local fabric and development storage (requires SQL Server 2008 Express), but I had lots of trouble working with local development storage for Azure Table, like exceptions as “Not able to connect to the server…”, “object doesn’t exist”, or simply failed silently ---- the data is just not there without any error message. After I created the Azure Storage Account and configured the sample application to use the “real” Azure table rather than the local simulation, everything moved smoothly. If you still want to get your hands dirty with the local development storage, the SQL scripts and database file are also included in the downloadable source code package.
The sample application is created using Cloud Service -> Web Cloud Service with a Web Role template in Visual Studio and named Azurelight. Then, we need to create a Silverlight 3 project (named AzurelightNav) based on the Silverlight Navigation Application template (note: not the Silverlight Business Application template installed by the RIA Services) and specify the Azure project’s WebRole ASP.NET project (named Azurelight_WebRole) as its hosting application and link to it.
After the initial start up, we also need to add the Azure class library to the solution. This is the class library included in Nikhil Kothari's Weblog download. It handles the low level communications with Azure Table Storage and provides the implementation of AzureDomainService and Azure Table based DataContext, Entity, and EntitySet types. This is the reason we didn’t use the Silverlight Business Application template to start our AzurelightNav Silverlight 3 application, and also, we don’t need to include the StorageClient class library from the Windows Azure SDK.
Since the server side data model will be “projected” to the client side as generated code by .NET RIA Services and the compiler, we need to set the client project (AzurelightNav) to depend on the server project (Azurelight_WebRole) in the solution’s Properties page. Otherwise, a compiler error will be generated after we define the model (client proxy is not generated).
Now that we have a skeleton of the Azure project up and running, let’s look at some details.
Configure the Services
Within the Azurelight Azure project, we need to add the following settings to the ServiceDefinition.csdef XML to enable the Azure Table configuration settings:
<!---->
<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>
The caveat in the service definition is not the three new configuration settings, it is that the enableNativeCodeExecution
needs to be explicitly set to be true for the WebRole. It essentially enables the full trust mode for the RIA Service on Azure. The default service definition has it to be false; RIA Services will not work with the default setting. Till .NET RIA Services is built on top of .NET 4.0, we need to explicitly enable it; see here for further information.
The actual value for these new configuration settings are set in the ServiceConfiguration.cscfg XML file:
<!---->
<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>
Just like what we discussed in Getting Started, I recommend to set up a Windows Azure Storage account to develop an Azure Table based project, instead of using the local development storage, to avoid some headaches that I ran into. As for the settings, the AccountName
is not your Azure Portal account name, but the name of your storage on Windows Azure. The AccountSharedKey
should be the Primary Access Key as listed in the summary page of your storage project in the Azure developer portal.
The account name and the account shared key also need to be set in the WebRole ASP.NET project’s Web.config file to enable the Azure class library to access the Azure Table account information:
<!---->
<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>
The caveat here is about the “uri=
” part: it must always be http://table.core.windows.net, or the runtime will throw an exception complaining it can’t find the service. This is not the same as the URL listed in Azure Portal about your table endpoint URL. That one has the account name, like http://azurelight.table.core.windows.net.
The storage account name, primary access key, and the shortened version of the table storage end point are crucial to access Azure Table storage from your project. Once we have the Azure Project and the WebRole ASP.NET configured, our data storage tier is ready to go, and we can move on to coding.
Define the Data Model and DataContext
Defining the application data model and the DataContext in the WebRole project is the first step to build our middle tier – the business logic tire. With .NET RIA Services and Azure Table storage, we can take the model-centric approach: define the application model first, then rely on the infrastructure to generate the Azure Table Structure based on our data model, since Azure Table doesn't require a pre-defined fixed data schema, while the database does.
Since our application domain is to collect and display the user’s feedback, we can create the simple Survey
class as our data model; the Survey.cs file is located under the folder named Azurelight_WebRole/Model.
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; }
}
}
The base class, Entity
, is an abstract class defined in the Azure class library. It makes sure all the derived types will have PartitionKey
, RowKey
, and time stamp properties and attributes. Those are required fields by Azure Table. Our derived type, Survey
, can focus on the domain model needs, rather than the underlining Azure Table specifics. In order to let Silverlight 3 DataForm
understand our intention to auto-generate data fields for input, we need to set the correct Display
attribute in the base Entity class (in the Azure class project).
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; }
}
}
Since the Survey
class is very simple, we don’t need a partial class to define its metadata; they are all defined in the same class file. Those metadata will be “projected” to the client, and the Silverlight 3 DataForm
control will generate the corresponding UI elements based on those metadata. And also, the default error handling for data validation (rating is required and must be within the specified range) will honor those metadata too.
[Bindable(false)]
and [Display(AutoGenerateField = false)]
essentially tell the DataForm control not to generate an input field for the property; it’s only used by business logic, and should be transparent for the end user. For example, the Target
property could refer to an application name, then our Survey
class can be used to collect feedbacks for different applications. And also, the PartitionKey
, RowKey
, and Timestamp
are all Azure Table specific fields, we don't want to expose them in the user interface.
With the context of .NET RIA Services, the application data model needs to be wrapped into a DataContext
derived type to adapt CRUD operations. Our SurveyDataContext.cs file is located under the same folder as where Survey.cs is:
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;
}
}
}
}
Again, the base class DataContext
is abstract
and defined in the same Azure class library; EntitySet is not abstract, its definition is in the same library. This is the Azure Table specific DataContext
and EntitySet
.
After we have a data model and data context, we can define our domain service type, which is where the business logic is exposed to the client for operations.
Create the Domain Service
The second step to develop our middle tier is to wrap the DataContext
by a DomainService
. The DomainService
will expose data/service operations to clients and delegate some operations to the DataContext
(the SurveyDataContext
we defined in the last section). We are still with the WebRole project, the domain service type is created under the Service folder:
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
public IQueryable<survey> GetSurveys()
{
return DataContext.Surveys;
}
public void AddSurvey(Survey survey)
{
survey.RowKey = Guid.NewGuid().ToString();
survey.Timestamp = DateTime.UtcNow;
DataContext.Surveys.InsertOnSubmit(survey);
}
public void UpdateSurvey(Survey currentSurvey)
{
DataContext.Surveys.Attach(currentSurvey,
originalSurvey, GetETag(currentSurvey));
}
#endregion
}
}
Just as the DataContext
type, the base AzureDomainService
is abstract
and comes from the Azure class library. This is an Azure Table storage specific DomainService, it doesn’t support the Azure Blob or Queue. It’s important to override the GetPartitionKey
method; it affects the PartitionKey
for all new entities to be stored in the Azure Table; it also affects the client query parameter.
Our implementation follows the "convention over configuration" approach. The data operation method names are GetXXX
, AddXXX
, and UpdateXXX
. They’ll be “projected” to the client generated code. If configuration is preferred, you can take the arbitrary method names then apply the [Query]
, [Create]
, [Update]
, and [ServiceOperation]
attributes to them. They’ll also be projected to the Silvelright 3 project.
Notice that most the data operations are delegated to the data context object. If your middle tier needs to expose domain-specific operations, you can define and implement them here and apply the correspondent [ServiceOperation]
attribute, so they’ll be visible to the client. For simplicity, this sample app doesn’t include other domain specific operations.
Within the body of the AddSurvey
method, we set the RowKey
and TimeStamp
to the survey object, then invoke the data context to add it to the list. The caveat is not to set the PartitionKey
, or the query (GetSurveys
) won’t find any entity, since the client query will use the return value of the GetPartitionKey
; if they are out of sync, you’ll have the problem that I had: saving always succeeds, but all queries return an empty list in the client.
Because the nature of the Survey application is to collect all kinds of feedback from the end user, we intentionally left out the Delete operation from the domain service. The end user can only view, add, and update feedback (ratings and comments) entries, not remove.
Another caveat is how to instantiate the instance of the domain service. I used to put the instantiation code in the Global.asax.cs::Application_Start
event handler, but starting debug will pop up an error message: “instances did not start within the time allowed”. Base on a post in MSDN, I moved the instantiation code to Application_BeginRequest
, then the error message disappeared. Please check out Global.asax.cs in the Azurelight_WebRole project for details.
Now that we have the data model, metadata, data context, domain service, and a working mechanism to instantiate them, compiling the project will “project” the model and the service to the client project (AzurelightNav). Let’s move our focus onto the presentation tier next.
Using the Projected Code in Silverlight
The compiler will create a hidden folder named Generated_Code in our client AzurelightNav project, and put the .NET RIA Services projected code (Azurelight_WebRole.g.cs) there. The Survey
(as data model) and SurveyContext
(as the delegate interacting with the server) are automatically available to the Silverlight code at development design time.
The sample project size is pretty small, we don’t really need a client side framework. But for demonstration purpose, I still added SilverlightCairngorm as the client side framework. It’s only 26K, and helps on separation of concerns for the Silverlight code. Since the projected/generated SurveyContext
already handles the interaction with the service, we don’t need any Cairngorm Delegate code; all Cairngorm Commands will execute the methods of SurveyContext
.
Since we’re using Silverlight Cairngorm, we won’t create the instance of SurveyContext
in XAML by the DomainDataSource
control, but rather, we have a read-only property inside AzurelightModel; it’ll serve as the root of all the survey data related binding data context in our views.
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" };
}
……
}
}
The Feedback
property is the data context for creating new and updated existing survey entries; CreateFeedback()
is a factory method to make sure all new Survey
instances will have a consistent target value across the application; the surveyLoaded
property is just a flag indicating the survey list data is already loaded from the service or not. All other details for AzurelightModel can be found at AzurelightNav/Model/AzurelightModel.cs in the downloaded source code.
Since we have the Model defined, we can create Views and data bind to the Model. Our primary view is Feedback.xaml (under the Views folder). It has a DataGrid
to display all the survey entries from the Azure Table (via SurveyDataContext
) and also three buttons to allow users to refresh, add, or update survey entries:
<!---->
<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>
The DataGrid
’s DataContext
will be set to the RIASvc
property of AzurelightModel
in the code-behind:
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)
{
CairngormEvent cgEvt =
new CairngormEvent(AzurelightController.SC_EVENT_LOAD_FEEDBACK);
cgEvt.dispatch();
}
private void btnAddClick(object sender, RoutedEventArgs e)
{
model.Feedback = model.CreateFeedback();
NewFeedBackPopup fbPopup = new NewFeedBackPopup(true);
fbPopup.Show();
}
private void btnUpdateClick(object sender, RoutedEventArgs e)
{
model.Feedback = feedbackGrid.SelectedItem as Survey;
NewFeedBackPopup fbPopup = new NewFeedBackPopup(false);
fbPopup.Show();
}
…
}
When adding new or updating existing survey entries, the button’s client event handler will launch a popup (NewFeedbackPopup.xaml):
<!---->
<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>
<!------------------------------- That's it! --------------------------->
AutoGenerateFields="True"
in the XAML tells the DataForm
control to generate data entry labels and fields based on the type of the feedback, which is the Survey
class we defined in the WebRole project. Its public bindable properties will auto-generate data bound fields, and metadata like [Required]
and [Range]
will trigger the visuals (bold label for required fields) and validation logic (error summary at the bottom and red border around the field when validation fails).
In the code-behind, it has a working mode for adding and updating, and when you commit the editing with no error, the correct Cairngorm Event will be raised to add or update:
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)
{
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();
}
}
}
As shown above, all event handlers code just dispatch different Cairngorm events; they don’t hold and interact with the SurveyDataContext
directly. The event will be routed by AzurelightController (AzurelightNav/Control/AzurelightController) to the corresponding command object to perform the actual work.
All commands are implemented under the AzurelightNav/Command folder; they just simply delete the method in the RIASvc
property of AzurelightModel
. SurveyDataContext
handles all the serialization, request and response events, de-serialization when receiving a response, and updated the property with load data. When a property is updated, the Silverlight Data Binding engine will notify the view to update itself automatically. With the help of the .NET RIA Services projected code, the presentation tier’s plumbing works are reduced and simplified greatly.
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);
}
}
}
The base class, FeedbackCmdBase
, defines common properties (model, progress dialog, event handler, etc.) and also handles SubmitChanges
(then it can be shared by the PostFeedback Command and the UpdateFeedback 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;
ChildWindow w = new ChildWindow()
{ Title = "Communication to Server Failed" };
w.Content = new TextBlock() { Text = "Error:" + Error.Message };
w.Show();
return false;
}
#endregion
}
}
Please check out the code in the Command, Control, and Model folders for all the details.
Up this point, we have a functioning application running Silverlight 3, .NET RIA Service, Azure Table storage at our local development machine. After testing, right click on the Azure project in Visual Studio, then select “Publish…”, and the deployment process to Windows Azure is pretty obvious.
The completed 3-tier application that runs with Silverlight 3, .NET RIA Services, Azure Table Storage, and hosted in Windows Azure, can be launched from http://azurelight.cloudapp.net/AzurelightNav.aspx#/Views/HomePage.xaml.
Wrapping up
This is an experiment sample project for RIA Services and Azure Table in Cloud. It demos the productivity gains when the application data model and the meta data is defined once, but is available for both the server and client side code, including auto-data entry fields generation, auto-error handling in the client, and the same validation logic written once and executed both in the client and server. But the developer experience can be improved as these new technologies evolve, like the concerns about the coupling in the service code and the client code, the required full-trust mode when running on Azure, the lack of official supporting libraries from the Azure SDK, hosted services Application ID decoupling, etc. I am looking forward to better services and support in the future.
History
- 07.09.2009 - Initial post.
- 07.11.2009 - Update for Silverlight 3 RTW, Silverlight 3 ToolKit July 2009, .NET RIA Services July 2009 Preview.