[Windows Azure] Building the web role for the Windows Azure Email Service application - 3 of 5
Building the web role for the Windows Azure Email Service application - 3 of 5.
This is the third tutorial in a series of five that show how to build and deploy the Windows Azure Email Service sample application. For information about the application and the tutorial series, see the first tutorial in the series.
In this tutorial you'll learn:
- How to create a solution that contains a Cloud Service project with a web role and a worker role.
 - How to work with Windows Azure tables, blobs, and queues in MVC 4 controllers and views.
 - How to handle concurrency conflicts when you are working with Windows Azure tables.
 - How to configure a web role or web project to use your Windows Azure Storage account.
 
Create solutionCreate the Visual Studio solution
You begin by creating a Visual Studio solution with a project for the web front-end and a project for one of the back-end Windows Azure worker roles. You'll add the second worker role later.
(If you want to run the web UI in a Windows Azure Web Site instead of a Windows Azure Cloud Service, see the Alternative Architecture section later in this tutorial for changes to these instructions.)
Create a cloud service project with a web role and a worker role
Start Visual Studio 2012 or Visual Studio 2012 for Web Express, with administrative privileges.
The Windows Azure compute emulator which enables you to test your cloud project locally requires administrative privileges.
From the File menu select New Project.

Expand C# and select Cloud under Installed Templates, and then select Windows Azure Cloud Service.
Name the application AzureEmailService and click OK.

In the New Windows Azure Cloud Service dialog box, select ASP.NET MVC 4 Web Role and click the arrow that points to the right.

In the column on the right, hover the pointer over MvcWebRole1, and then click the pencil icon to change the name of the web role.
Enter MvcWebRole as the new name, and then press Enter.

Follow the same procedure to add a Worker Role, name it WorkerRoleA, and then click OK.

In the New ASP.NET MVC 4 Project dialog box, select the Internet Application template.
In the View Engine drop-down list make sure that Razor is selected, and then click OK.

Set the page header, menu, and footer
In this section you update the headers, footers, and menu items that are shown on every page for the administrator web UI. The application will have three sets of administrator web pages: one for Mailing Lists, one for Subscribers to mailing lists, and one for Messages.
In Solution Explorer, expand the Views\Shared folder and open the _Layout.cshtml file.

In the <title> element, change "My ASP.NET MVC Application" to "Windows Azure Email Service".
In the <p> element with class "site-title", change "your logo here" to "Windows Azure Email Service", and change "Home" to "MailingList".

Delete the menu section:

Insert a new menu section where the old one was:
<ulid="menu"><li>@Html.ActionLink("Mailing Lists", "Index", "MailingList")</li><li>@Html.ActionLink("Messages", "Index", "Message")</li><li>@Html.ActionLink("Subscribers", "Index", "Subscriber")</li></ul>In the <footer> element, change "My ASP.NET MVC Application" to "Windows Azure Email Service".

Run the application locally
Press CTRL+F5 to run the application.
The application home page appears in the default browser.

The application runs in the Windows Azure compute emulator. You can see the compute emulator icon in the Windows system tray:
![Compute emulator in system tray][mtas-compute-emulator-icon]
Configure TracingConfigure Tracing
To enable tracing data to be saved, open the WebRole.cs file and add the following ConfigureDiagnostics method. Add code that calls the new method in the OnStart method.
privatevoidConfigureDiagnostics(){DiagnosticMonitorConfiguration config =DiagnosticMonitor.GetDefaultInitialConfiguration();
    config.Logs.BufferQuotaInMB=500;
    config.Logs.ScheduledTransferLogLevelFilter=LogLevel.Verbose;
    config.Logs.ScheduledTransferPeriod=TimeSpan.FromMinutes(1d);DiagnosticMonitor.Start("Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString",
        config);}publicoverrideboolOnStart(){ConfigureDiagnostics();returnbase.OnStart();}
The ConfigureDiagnostics method is explained in the second tutorial.
RestartsAdd code to efficiently handle restarts.
Windows Azure Cloud Service applications are restarted approximately twice per month for operating system updates. (For more information on OS updates, see Role Instance Restarts Due to OS Upgrades.) When a web application is going to be shut down, an OnStop event is raised. The web role boiler plate created by Visual Studio does not override the OnStop method, so the application will have only a few seconds to finish processing HTTP requests before it is shut down. You can add code to override the OnStop method in order to ensure that shutdowns are handled gracefully.
To handle shutdowns and restarts, open the WebRole.cs file and add the following OnStop method override.
publicoverridevoidOnStop(){Trace.TraceInformation("OnStop called from WebRole");var rcCounter =newPerformanceCounter("ASP.NET","Requests Current","");while(rcCounter.NextValue()>0){Trace.TraceInformation("ASP.NET Requests Current = "+ rcCounter.NextValue().ToString());System.Threading.Thread.Sleep(1000);}}
This code requires an additional using statement:
usingSystem.Diagnostics;
The OnStop method has up to 5 minutes to exit before the application is shut down. You could add a sleep call for 5 minutes to the OnStop method to give your application the maximum amount of time to process the current requests, but if your application is scaled correctly, it should be able to process the remaining requests in much less than 5 minutes. It is best to stop as quickly as possible, so that the application can restart as quickly as possible and continue processing requests.
Once a role is taken off-line by Windows Azure, the load balancer stops sending requests to the role instance, and after that the OnStop method is called. If you don't have another instance of your role, no requests will be processed until your role completes shutting down and is restarted (which typically takes several minutes). That is one reason why the Windows Azure service level agreement requires you to have at least two instances of each role in order to take advantage of the up-time guarantee.
In the code shown for the OnStop method, an ASP.NET performance counter is created for Requests Current. The Requests Current counter value contains the current number of requests, including those that are queued, currently executing, or waiting to be written to the client. The Requests Current value is checked every second, and once it falls to zero, the OnStop method returns. Once OnStop returns, the role shuts down.
Trace data is not saved when called from the OnStop method without performing an On-Demand Transfer. You can view the OnStop trace information in real time with the dbgview utility from a remote desktop connection.
Update Storage Client LibraryUpdate the Storage Client Library NuGet Package
The API framework that you use to work with Windows Azure Storage tables, queues, and blobs is the Storage Client Library (SCL). This API is included in a NuGet package in the Cloud Service project template. However, as of the date this tutorial is being written, the project templates include the 1.7 version of SCL, not the current 2.0 version. Therefore, before you begin writing code you'll update the NuGet package.
In the Visual Studio Tools menu, hover over Library Package Manager, and then click Manage NuGet Packages for Solution.

In the left pane of the Manage NuGet Packages dialog box, select Updates, then scroll down to the Windows Azure Storage package and click Update.

In the Select Projects dialog box, make sure both projects are selected, and then click OK.

Accept the license terms to complete installation of the package, and then close the Manage NuGet Packages dialog box.
In WorkerRoleA.cs in the WorkerRoleA project, delete the following
usingstatement because it is no longer needed:usingMicrosoft.WindowsAzure.StorageClient;
The 1.7 version of the SCL includes a LINQ provider that simplifies coding for table queries. As of the date this tutorial is being written, the 2.0 Table Service Layer (TSL) does not yet have a LINQ provider. If you want to use LINQ, you still have access to the SCL 1.7 LINQ provider in the Microsoft.WindowsAzure.Storage.Table.DataServices namespace. The 2.0 TSL was designed to improve performance, and the 1.7 LINQ provider does not benefit from all of these improvements. The sample application uses the 2.0 TSL, so it does not use LINQ for queries. For more information about SCL and TSL 2.0, see the resources at the end of the last tutorial in this series.
Add SCL 1.7 referenceAdd a reference to an SCL 1.7 assembly
Version 2.0 of the Storage Client Library (SCL) 2.0 does not have everything needed for diagnostics, so you have to add a reference to a 1.7 assembly.
Right-click the MvcWebRole project, and choose Add Reference.
Click the Browse... button at the bottom of the dialog box.
Navigate to the following folder:
C:\Program Files\Microsoft SDKs\Windows Azure\.NET SDK\2012-10\ref
Select Microsoft.WindowsAzure.StorageClient.dll, and then click Add.
In the Reference Manager dialog box, click OK.
Repeat the process for the WorkerRoleA project.
App_Start CodeAdd code to create tables, queue, and blob container in the Application_Start method
The web application will use the MailingList table, the Message table, the azuremailsubscribequeue queue, and the azuremailblobcontainer blob container. You could create these manually by using a tool such as Azure Storage Explorer, but then you would have to do that manually every time you started to use the application with a new storage account. In this section you'll add code that runs when the application starts, checks if the required tables, queues, and blob containers exist, and creates them if they don't.
You could add this one-time startup code to the OnStart method in the WebRole.cs file, or to the Global.asax file. For this tutorial you'll initialize Windows Azure Storage in the Global.asax file since that works with Windows Azure Web Sites as well as Windows Azure Cloud Service web roles.
In Solution Explorer, expand Global.asax and then open Global.asax.cs.
Add a new
CreateTablesQueuesBlobContainersmethod after theApplication_Startmethod, and then call the new method from theApplication_Startmethod, as shown in the following example:protectedvoidApplication_Start(){AreaRegistration.RegisterAllAreas();WebApiConfig.Register(GlobalConfiguration.Configuration);FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);RouteConfig.RegisterRoutes(RouteTable.Routes);BundleConfig.RegisterBundles(BundleTable.Bundles);AuthConfig.RegisterAuth();// Verify that all of the tables, queues, and blob containers used in this application// exist, and create any that don't already exist.CreateTablesQueuesBlobContainers();}privatestaticvoidCreateTablesQueuesBlobContainers(){var storageAccount =CloudStorageAccount.Parse(RoleEnvironment.GetConfigurationSettingValue("StorageConnectionString"));// If this is running in a Windows Azure Web Site (not a Cloud Service) use the Web.config file:// var storageAccount = CloudStorageAccount.Parse(ConfigurationManager.ConnectionStrings["StorageConnectionString"].ConnectionString);var tableClient = storageAccount.CreateCloudTableClient();var mailingListTable = tableClient.GetTableReference("MailingList");
mailingListTable.CreateIfNotExists();var messageTable = tableClient.GetTableReference("Message");
messageTable.CreateIfNotExists();var blobClient = storageAccount.CreateCloudBlobClient();var blobContainer = blobClient.GetContainerReference("azuremailblobcontainer");
blobContainer.CreateIfNotExists();var queueClient = storageAccount.CreateCloudQueueClient();var subscribeQueue = queueClient.GetQueueReference("azuremailsubscribequeue");
subscribeQueue.CreateIfNotExists();}Right click on the blue squiggly line under
RoleEnvironment, select Resolve then select using Microsoft.WindowsAzure.ServiceRuntime.
Right click the blue squiggly line under
CloudStorageAccount, select Resolve, and then select using Microsoft.WindowsAzure.Storage.Alternatively, you can manually add the following using statements:
usingMicrosoft.WindowsAzure.ServiceRuntime;usingMicrosoft.WindowsAzure.Storage;
Build the application, which saves the file and verifies that you don't have any compile errors.
In the following sections you build the components of the web application, and you can test them with development storage or your storage account without having to manually create tables, queues, or blob container first.
Mailing ListCreate and test the Mailing List controller and views
The Mailing List web UI is used by administrators to create, edit and display mailing lists, such as "Contoso University History Department announcements" and "Fabrikam Engineering job postings".
Add the MailingList entity class to the Models folder
The MailingList entity class is used for the rows in the MailingList table that contain information about the list, such as its description and the "From" email address for emails sent to the list.
In Solution Explorer, right-click the
Modelsfolder in the MVC project, and choose Add Existing Item.
Navigate to the folder where you downloaded the sample application, select the MailingList.cs file in the
Modelsfolder, and click Add.Open MailingList.cs and examine the code.
publicclassMailingList:TableEntity{publicMailingList(){this.RowKey="mailinglist";}[Required][RegularExpression(@"[\w]+",ErrorMessage=@"Only alphanumeric characters and underscore (_) are allowed.")][Display(Name="List Name")]publicstringListName{get{returnthis.PartitionKey;}set{this.PartitionKey= value;}}[Required][Display(Name="'From' Email Address")]publicstringFromEmailAddress{get;set;}publicstringDescription{get;set;}}The Windows Azure Storage TSL 2.0 API requires that the entity classes that you use for table operations derive from TableEntity. This class defines
PartitionKey,RowKey,TimeStamp, andETagfields. TheTimeStampandETagproperties are used by the system. You'll see how theETagproperty is used for concurrency handling later in the tutorial.(There is also a DynamicTableEntity class for use when you want to work with table rows as Dictionary collections of key value pairs instead of by using predefined model classes. For more information, see Windows Azure Storage Client Library 2.0 Tables Deep Dive.)
The
mailinglisttable partition key is the list name. In this entity class the partition key value can be accessed either by using thePartitionKeyproperty (defined in theTableEntityclass) or theListNameproperty (defined in theMailingListclass). TheListNameproperty usesPartitionKeyas its backing variable. Defining theListNameproperty enables you to use a more descriptive variable name in code and makes it easier to program the web UI, since formatting and validation DataAnnotations attributes can be added to theListNameproperty, but they can't be added directly to thePartitionKeyproperty.The
RegularExpressionattribute on theListNameproperty causes MVC to validate user input to ensure that the list name value entered only contains alphanumeric characters or underscores. This restriction was implemented in order to keep list names simple so that they can easily be used in query strings in URLs.Note: If you wanted the list name format to be less restrictive, you could allow other characters and URL-encode list names when they are used in query strings. However, certain characters are not allowed in Windows Azure Table partition keys or row keys, and you would have to exclude at least those characters. For information about characters that are not allowed or cause problems in the partition key or row key fields, see Understanding the Table Service Data Model and % Character in PartitionKey or RowKey.
The
MailingListclass defines a default constructor that setsRowKeyto the hard-coded string "mailinglist", because all of the mailing list rows in this table have that value as their row key. (For an explanation of the table structure, see the first tutorial in the series.) Any constant value could have been chosen for this purpose, as long as it could never be the same as an email address, which is the row key for the subscriber rows in this table.The list name and the "from" email address must always be entered when a new
MailingListentity is created, so they haveRequiredattributes.The
Displayattributes specify the default caption to be used for a field in the MVC UI.
Add the MailingList MVC controller
In Solution Explorer, right-click the Controllers folder in the MVC project, and choose Add Existing Item.

Navigate to the folder where you downloaded the sample application, select the MailingListController.cs file in the
Controllersfolder, and click Add.Open MailingListController.cs and examine the code.
The default constructor creates a
CloudTableobject to use for working with themailinglisttable.publicclassMailingListController:Controller{privateCloudTable mailingListTable;publicMailingListController(){var storageAccount =Microsoft.WindowsAzure.Storage.CloudStorageAccount.Parse(RoleEnvironment.GetConfigurationSettingValue("StorageConnectionString"));// If this is running in a Windows Azure Web Site (not a Cloud Service) use the Web.config file:// var storageAccount = Microsoft.WindowsAzure.Storage.CloudStorageAccount.Parse(ConfigurationManager.ConnectionStrings["StorageConnectionString"].ConnectionString);var tableClient = storageAccount.CreateCloudTableClient();
mailingListTable = tableClient.GetTableReference("mailinglist");}The code gets the credentials for your Windows Azure Storage account from the Cloud Service project settings file in order to make a connection to the storage account. (You'll configure those settings later in this tutorial, before you test the controller.) If you are going to run the MVC project in a Windows Azure Web Site, you can get the connection string from the Web.config file instead.
Next is a
FindRowmethod that is called whenever the controller needs to look up a specific mailing list entry of theMailingListtable, for example to edit a mailing list entry. The code retrieves a singleMailingListentity by using the partition key and row key values passed in to it. The rows that this controller edits are the ones that have "MailingList" as the row key, so "MailingList" could have been hard-coded for the row key, but specifying both partition key and row key is a pattern used for theFindRowmethods in all of the controllers.privateMailingListFindRow(string partitionKey,string rowKey){var retrieveOperation =TableOperation.Retrieve<MailingList>(partitionKey, rowKey);var retrievedResult = mailingListTable.Execute(retrieveOperation);var mailingList = retrievedResult.ResultasMailingList;if(mailingList ==null){thrownewException("No mailing list found for: "+ partitionKey);}return mailingList;}It's instructive to compare the
FindRowmethod in theMailingListcontroller, which returns a mailing list row, with theFindRowmethod in theSubscribercontroller, which returns a subscriber row from the samemailinglisttable.privateSubscriberFindRow(string partitionKey,string rowKey){var retrieveOperation =TableOperation.Retrieve<Subscriber>(partitionKey, rowKey);var retrievedResult = mailingListTable.Execute(retrieveOperation);var subscriber = retrievedResult.ResultasSubscriber;if(subscriber ==null){thrownewException("No subscriber found for: "+ partitionKey +", "+ rowKey);}return subscriber;}The only difference in the two queries is the model type that they pass to the TableOperation.Retrieve method. The model type specifies the schema (the properties) of the row or rows that you expect the query to return. A single table may have different schemas in different rows. Typically you specify the same model type when reading a row that was used to create the row.
The Index page displays all of the mailing list rows, so the query in the
Indexmethod returns allMailingListentities that have "mailinglist" as the row key (the other rows in the table have email address as the row key, and they contain subscriber information).var query =newTableQuery<MailingList>().Where(TableQuery.GenerateFilterCondition("RowKey",QueryComparisons.Equal,"mailinglist"));
lists = mailingListTable.ExecuteQuery(query, reqOptions).ToList();The
Indexmethod surrounds this query with code that is designed to handle timeout conditions.publicActionResultIndex(){TableRequestOptions reqOptions =newTableRequestOptions(){MaximumExecutionTime=TimeSpan.FromSeconds(1.5),RetryPolicy=newLinearRetry(TimeSpan.FromSeconds(3),3)};List<MailingList> lists;try{var query =newTableQuery<MailingList>().Where(TableQuery.GenerateFilterCondition("RowKey",QueryComparisons.Equal,"mailinglist"));
lists = mailingListTable.ExecuteQuery(query, reqOptions).ToList();}catch(StorageException se){ViewBag.errorMessage ="Timeout error, try again. ";Trace.TraceError(se.Message);returnView("Error");}returnView(lists);}If you don't specify timeout parameters, the API automatically retries three times with exponentially increasing timeout limits. For a web interface with a user waiting for a page to appear, this could result in unacceptably long wait times. Therefore, this code specifies linear retries (so the timeout limit doesn't increase each time) and a timeout limit that is reasonable for the user to wait.
When the user clicks the Create button on the Create page, the MVC model binder creates a
MailingListentity from input entered in the view, and theHttpPost Createmethod adds the entity to the table.[HttpPost][ValidateAntiForgeryToken]publicActionResultCreate(MailingList mailingList){if(ModelState.IsValid){var insertOperation =TableOperation.Insert(mailingList);
mailingListTable.Execute(insertOperation);returnRedirectToAction("Index");}returnView(mailingList);}For the Edit page, the
HttpGet Editmethod looks up the row, and theHttpPostmethod updates the row.[HttpPost][ValidateAntiForgeryToken]publicActionResultEdit(string partitionKey,string rowKey,MailingList editedMailingList){if(ModelState.IsValid){var mailingList =newMailingList();UpdateModel(mailingList);try{var replaceOperation =TableOperation.Replace(mailingList);
mailingListTable.Execute(replaceOperation);returnRedirectToAction("Index");}catch(StorageException ex){if(ex.RequestInformation.HttpStatusCode==412){// Concurrency errorvar currentMailingList =FindRow(partitionKey, rowKey);if(currentMailingList.FromEmailAddress!= editedMailingList.FromEmailAddress){ModelState.AddModelError("FromEmailAddress","Current value: "+ currentMailingList.FromEmailAddress);}if(currentMailingList.Description!= editedMailingList.Description){ModelState.AddModelError("Description","Current value: "+ currentMailingList.Description);}ModelState.AddModelError(string.Empty,"The record you attempted to edit "+"was modified by another user after you got the original value. The "+"edit operation was canceled and the current values in the database "+"have been displayed. If you still want to edit this record, click "+"the Save button again. Otherwise click the Back to List hyperlink.");ModelState.SetModelValue("ETag",newValueProviderResult(currentMailingList.ETag, currentMailingList.ETag,null));}else{throw;}}}returnView(editedMailingList);}The try-catch block handles concurrency errors. A concurrency exception is raised if a user selects a mailing list for editing, then while the Edit page is displayed in the browser another user edits the same mailing list. When that happens, the code displays a warning message and indicates which fields were changed by the other user. The TSL API uses the
ETagto check for concurrency conflicts. Every time a table row is updated, theETagvalue is changed. When you get a row to edit, you save theETagvalue, and when you execute an update or delete operation you pass in theETagvalue that you saved. (TheEditview has a hidden field for the ETag value.) If the update operation finds that theETagvalue on the record you are updating is different than theETagvalue that you passed in to the update operation, it raises a concurrency exception. If you don't care about concurrency conflicts, you can set the ETag field to an asterisk ("*") in the entity that you pass in to the update operation, and conflicts are ignored.Note: The HTTP 412 error is not unique to concurrency errors. It can be raised for other errors by the SCL API.
For the Delete page, the
HttpGet Deletemethod looks up the row in order to display its contents, and theHttpPostmethod deletes theMailingListrow along with anySubscriberrows that are associated with it in theMailingListtable.[HttpPost,ActionName("Delete")][ValidateAntiForgeryToken]publicActionResultDeleteConfirmed(string partitionKey){// Delete all rows for this mailing list, that is, // Subscriber rows as well as MailingList rows.// Therefore, no need to specify row key.var query =newTableQuery<MailingList>().Where(TableQuery.GenerateFilterCondition("PartitionKey",QueryComparisons.Equal, partitionKey));var listRows = mailingListTable.ExecuteQuery(query).ToList();var batchOperation =newTableBatchOperation();int itemsInBatch =0;foreach(MailingList listRow in listRows){
batchOperation.Delete(listRow);
itemsInBatch++;if(itemsInBatch ==100){
mailingListTable.ExecuteBatch(batchOperation);
itemsInBatch =0;
batchOperation =newTableBatchOperation();}}if(itemsInBatch >0){
mailingListTable.ExecuteBatch(batchOperation);}returnRedirectToAction("Index");}In case a large number of subscribers need to be deleted, the code deletes the records in batches. The transaction cost of deleting one row is the same as deleting 100 rows in a batch. The maximum number of operations that you can perform in one batch is 100.
Although the loop processes both
MailingListrows andSubscriberrows, it reads them all into theMailingListentity class because the only fields needed for theDeleteoperation are thePartitionKey,RowKey, andETagfields.
Add the MailingList MVC views
In Solution Explorer, create a new folder under the Views folder in the MVC project, and name it MailingList.
Right-click the new Views\MailingList folder, and choose Add Existing Item.

Navigate to the folder where you downloaded the sample application, select all four of the .cshtml files in the Views\MailingList folder, and click Add.
Open the Edit.cshtml file and examine the code.
@modelMvcWebRole.Models.MailingList@{ViewBag.Title="Edit Mailing List";}<h2>EditMailingList</h2>
@using (Html.BeginForm()) {
@Html.AntiForgeryToken()
@Html.ValidationSummary(true)
@Html.HiddenFor(model => model.ETag)
<fieldset>
<legend>MailingList</legend><div class="editor-label">@Html.LabelFor(model => model.ListName)</div>
<div class="editor-field">
@Html.DisplayFor(model => model.ListName)
</div><div class="editor-label">@Html.LabelFor(model => model.Description)</div>
<div class="editor-field">
@Html.EditorFor(model => model.Description)
@Html.ValidationMessageFor(model => model.Description)
</div><div class="editor-label">@Html.LabelFor(model => model.FromEmailAddress)</div>
<div class="editor-field">
@Html.EditorFor(model => model.FromEmailAddress)
@Html.ValidationMessageFor(model => model.FromEmailAddress)
</div><p><input type="submit" value="Save"/></p>
</fieldset>}<div>@Html.ActionLink("Back to List","Index")</div>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}This code is typical for MVC views. Notice the hidden field that is included to preserve the
ETagvalue which is used for handling concurrency conflicts. Notice also that theListNamefield has aDisplayForhelper instead of anEditorForhelper. We didn't enable the Edit page to change the list name, because that would have required complex code in the controller: theHttpPost Editmethod would have had to delete the existing mailing list row and all associated subscriber rows, and re-insert them all with the new key value. In a production application you might decide that the additional complexity is worthwhile. As you'll see later, theSubscribercontroller does allow list name changes, since only one row at a time is affected.The Create.cshtml and Delete.cshtml code is similar to Edit.cshtml.
Open Index.cshtml and examine the code.
@modelIEnumerable<MvcWebRole.Models.MailingList>@{ViewBag.Title="Mailing Lists";}<h2>MailingLists</h2>
<p>
@Html.ActionLink("Create New", "Create")
</p><table><tr><th>@Html.DisplayNameFor(model => model.ListName)</th>
<th>
@Html.DisplayNameFor(model => model.Description)
</th><th>@Html.DisplayNameFor(model => model.FromEmailAddress)</th>
<th></th></tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.ListName)
</td><td>@Html.DisplayFor(modelItem => item.Description)</td>
<td>
@Html.DisplayFor(modelItem => item.FromEmailAddress)
</td><td>@Html.ActionLink("Edit","Edit",new{PartitionKey= item.PartitionKey,RowKey=item.RowKey})|@Html.ActionLink("Delete","Delete",new{PartitionKey= item.PartitionKey,RowKey=item.RowKey})</td>
</tr>}</table>This code is also typical for MVC views. The Edit and Delete hyperlinks specify partition key and row key query string parameters in order to identify a specific row. For
MailingListentities only the partition key is actually needed since row key is always "MailingList", but both are kept so that the MVC view code is consistent across all controllers and views.
Make MailingList the default controller
Open Route.config.cs in the App_Start folder.
In the line that specifies defaults, change the default controller from "Home" to "MailingList".
routes.MapRoute(
name:"Default",
url:"{controller}/{action}/{id}",
defaults:new{ controller ="MailingList", action ="Index", id =UrlParameter.Optional}
Configure storageConfigure the web role to use your test Windows Azure Storage account
You are going to enter settings for your test storage account, which you will use while running the project locally. To add a new setting you have to add it for both cloud and local, but you can change the cloud value later. You'll add the same settings for worker role A later.
(If you want to run the web UI in a Windows Azure Web Site instead of a Windows Azure Cloud Service, see the Alternative Architecture section later in this tutorial for changes to these instructions.)
In Solution Explorer, right-click MvcWebRole under Roles in the AzureEmailService cloud project, and then choose Properties.

Make sure that All Configurations is selected in the Service Configuration drop-down list.
Select the Settings tab and then click Add Setting.
Enter "StorageConnectionString" in the Name column.
Select Connection String in the Type drop-down list.
Click the ellipsis (...) button at the right end of the line to open the Storage Account Connection String dialog box.

In the Create Storage Connection String dialog, click the Your subscription radio button, and then click the Download Publish Settings link.
Note: If you configured storage settings for tutorial 2 and you're doing this tutorial on the same machine, you don't have to download the settings again, you just have to click Your subscription and then choose the correct Subscription and Account Name.

When you click the Download Publish Settings link, Visual Studio launches a new instance of your default browser with the URL for the Windows Azure Management Portal download publish settings page. If you are not logged into the portal, you are prompted to log in. Once you are logged in your browser prompts you to save the publish settings. Make a note of where you save the settings.

In the Create Storage Connection String dialog, click Import, and then navigate to the publish settings file that you saved in the previous step.
Select the subscription and storage account that you wish to use, and then click OK.

Follow the same procedure that you used for the
StorageConnectionStringconnection string to set theMicrosoft.WindowsAzure.Plugins.Diagnostics.ConnectionStringconnection string.You don't have to download the publish settings file again. When you click the ellipsis for the
Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionStringconnection string, you'll find that the Create Storage Connection String dialog box remembers your subscription information. When you click the Your subscription radio button, all you have to do is select the same Subscription and Account Name that you selected earlier, and then click OK.Follow the same procedure that you used for the two connection strings for the MvcWebRole role to set the connection strings for the WorkerRoleA role.
When you added a new setting with the Add Settings button, the new setting was added to the XML in the ServiceDefinition.csdf file and in each of the two .cscfg configuration files. The following XML is added by Visual Studio to the ServiceDefinition.csdf file.
<ConfigurationSettings><Settingname="StorageConnectionString"/></ConfigurationSettings>
The following XML is added to each .cscfg configuration file.
<Settingname="StorageConnectionString"value="DefaultEndpointsProtocol=https;
AccountName=azuremailstorage;
AccountKey=[your account key]"/>
You can manually add settings to the ServiceDefinition.csdf file and the two .cscfg configuration files, but using the properties editor has the following advantages for connection strings:
- You only add the new setting in one place, and the correct setting XML is added to all three files.
 The correct XML is generated for the three settings files. The ServiceDefinition.csdf file defines settings that must be in each .cscfg configuration file. If the ServiceDefinition.csdf file and the two .cscfg configuration files settings are inconsistent, you can get the following error message from Visual Studio: "The current service model is out of sync. Make sure both the service configuration and definition files are valid."

If you get this error, the properties editor will not work until you resolve the inconsistency problem.
Test the application
Run the project by pressing CTRL+F5.

Use the Create function to add some mailing lists, and try the Edit and Delete functions to make sure they work.

SubscriberCreate and test the Subscriber controller and views
The Subscriber web UI is used by administrators to add new subscribers to a mailing list, and to edit, display, and delete existing subscribers.
Add the Subscriber entity class to the Models folder
The Subscriber entity class is used for the rows in the MailingList table that contain information about subscribers to a list. These rows contain information such as the person's email address and whether the address is verified.
In Solution Explorer, right-click the Models folder in the MVC project, and choose Add Existing Item.
Navigate to the folder where you downloaded the sample application, select the Subscriber.cs file in the Models folder, and click Add.
Open Subscriber.cs and examine the code.
publicclassSubscriber:TableEntity{[Required]publicstringListName{get{returnthis.PartitionKey;}set{this.PartitionKey= value;}}[Required][Display(Name="Email Address")]publicstringEmailAddress{get{returnthis.RowKey;}set{this.RowKey= value;}}publicstringSubscriberGUID{get;set;}publicbool?Verified{get;set;}}Like the
MailingListentity class, theSubscriberentity class is used to read and write rows in themailinglisttable.Subscriberrows use the email address instead of the constant "mailinglist" for the row key. (For an explanation of the table structure, see the first tutorial in the series.) Therefore anEmailAddressproperty is defined that uses theRowKeyproperty as its backing field, the same way thatListNameusesPartitionKeyas its backing field. As explained earlier, this enables you to put formatting and validation DataAnnotations attributes on the properties.The
SubscriberGUIDvalue is generated when aSubscriberentity is created. It is used in subscribe and unsubscribe links to help ensure that only authorized persons can subscribe or unsubscribe email addresses.When a row is initially created for a new subscriber, the
Verifiedvalue isfalse. TheVerifiedvalue changes totrueonly after the new subscriber clicks the Confirm hyperlink in the welcome email. If a message is sent to a list while a subscriber hasVerified=false, no email is sent to that subscriber.The
Verifiedproperty in theSubscriberentity is defined as nullable. When you specify that a query should returnSubscriberentities, it is possible that some of the retrieved rows might not have aVerifiedproperty. Therefore theSubscriberentity defines itsVerifiedproperty as nullable so that it can more accurately reflect the actual content of a row if table rows that don't have a Verified property are returned by a query. You might be accustomed to working with SQL Server tables, in which every row of a table has the same schema. In a Windows Azure Storage table, each row is just a collection of properties, and each row can have a different set of properties. For example, in the Windows Azure Email Service sample application, rows that have "MailingList" as the row key don't have aVerifiedproperty. If a query returns a table row that doesn't have aVerifiedproperty, when theSubscriberentity class is instantiated, theVerifiedproperty in the entity object will be null. If the property were not nullable, you would get the same value offalsefor rows that haveVerified=falseand for rows that don't have aVerifiedproperty at all. Therefore, a best practice for working with Windows Azure Tables is to make each property of an entity class nullable in order to accurately read rows that were created by using different entity classes or different versions of the current entity class.
Add the Subscriber MVC controller
In Solution Explorer, right-click the Controllers folder in the MVC project, and choose Add Existing Item.
Navigate to the folder where you downloaded the sample application, select the SubscriberController.cs file in the Controllers folder, and click Add. (Make sure that you get Subscriber.cs and not Subscribe.cs; you'll add Subscribe.cs later.)
Open SubscriberController.cs and examine the code.
Most of the code in this controller is similar to what you saw in the
MailingListcontroller. Even the table name is the same because subscriber information is kept in theMailingListtable. After theFindRowmethod you see aGetListNamesmethod. This method gets the data for a drop-down list on the Create and Edit pages, from which you can select the mailing list to subscribe an email address to.privateList<MailingList>GetListNames(){var query =(newTableQuery<MailingList>().Where(TableQuery.GenerateFilterCondition("RowKey",QueryComparisons.Equal,"mailinglist")));var lists = mailingListTable.ExecuteQuery(query).ToList();return lists;}This is the same query you saw in the
MailingListcontroller. For the drop-down list you want rows that have information about mailing lists, so you select only those that have RowKey = "mailinglist".For the method that retrieves data for the Index page, you want rows that have subscriber information, so you select all rows that do not have RowKey = "MailingList".
publicActionResultIndex(){var query =(newTableQuery<Subscriber>().Where(TableQuery.GenerateFilterCondition("RowKey",QueryComparisons.NotEqual,"mailinglist")));var subscribers = mailingListTable.ExecuteQuery(query).ToList();returnView(subscribers);}Notice that the query specifies that data will be read into
Subscriberobjects (by specifying<Subscriber>) but the data will be read from themailinglisttable.Note: The number of subscribers could grow to be too large to handle this way in a single query. In a future release of the tutorial we hope to implement paging functionality and show how to handle continuation tokens. You need to handle continuation tokens when you execute queries that would return more than 1,000 rows: Windows Azure returns 1,000 rows and a continuation token that you use to execute another query that starts where the previous one left off. (Azure Storage Explorer does not handle continuation tokens; therefore its queries will not return more than 1,000 rows.) For more information about large result sets and continuation tokens, see How to get most out of Windows Azure Tables and Windows Azure Tables: Expect Continuation Tokens, Seriously.
In the
HttpGet Createmethod, you set up data for the drop-down list; and in theHttpPostmethod, you set default values before saving the new entity.publicActionResultCreate(){var lists =GetListNames();ViewBag.ListName=newSelectList(lists,"ListName","Description");var model =newSubscriber(){Verified=false};returnView(model);}[HttpPost][ValidateAntiForgeryToken]publicActionResultCreate(Subscriber subscriber){if(ModelState.IsValid){
subscriber.SubscriberGUID=Guid.NewGuid().ToString();if(subscriber.Verified.HasValue==false){
subscriber.Verified=false;}var insertOperation =TableOperation.Insert(subscriber);
mailingListTable.Execute(insertOperation);returnRedirectToAction("Index");}var lists =GetListNames();ViewBag.ListName=newSelectList(lists,"ListName","Description", subscriber.ListName);returnView(subscriber);}The
HttpPost Editpage is more complex than what you saw in theMailingListcontroller because theSubscriberpage enables you to change the list name or email address, both of which are key fields. If the user changes one of these fields, you have to delete the existing record and add a new one instead of updating the existing record. The following code shows the part of the edit method that handles the different procedures for key versus non-key changes:if(ModelState.IsValid){try{UpdateModel(editedSubscriber,string.Empty,null, excludeProperties);if(editedSubscriber.PartitionKey== partitionKey && editedSubscriber.RowKey== rowKey){//Keys didn't change -- Update the rowvar replaceOperation =TableOperation.Replace(editedSubscriber);
mailingListTable.Execute(replaceOperation);}else{// Keys changed, delete the old record and insert the new one.if(editedSubscriber.PartitionKey!= partitionKey){// PartitionKey changed, can't do delete/insert in a batch.var deleteOperation =TableOperation.Delete(newSubscriber{PartitionKey= partitionKey,RowKey= rowKey,ETag= editedSubscriber.ETag});
mailingListTable.Execute(deleteOperation);var insertOperation =TableOperation.Insert(editedSubscriber);
mailingListTable.Execute(insertOperation);}else{// RowKey changed, do delete/insert in a batch.var batchOperation =newTableBatchOperation();
batchOperation.Delete(newSubscriber{PartitionKey= partitionKey,RowKey= rowKey,ETag= editedSubscriber.ETag});
batchOperation.Insert(editedSubscriber);
mailingListTable.ExecuteBatch(batchOperation);}}returnRedirectToAction("Index");The parameters that the MVC model binder passes to the
Editmethod include the original list name and email address values (in thepartitionKeyandrowKeyparameters) and the values entered by the user (in thelistNameandemailAddressparameters):publicActionResultEdit(string partitionKey,string rowKey,string listName,string emailAddress)
The parameters passed to the
UpdateModelmethod excludePartitionKeyandRowKeyproperties from model binding:var excludeProperties =newstring[]{"PartitionKey","RowKey"};The reason for this is that the
ListNameandEmailAddressproperties usePartitionKeyandRowKeyas their backing properties, and the user might have changed one of these values. When the model binder updates the model by setting theListNameproperty, thePartitionKeyproperty is automatically updated. If the model binder were to update thePartitionKeyproperty with that property's original value after updating theListNameproperty, it would overwrite the new value that was set by theListNameproperty. TheEmailAddressproperty automatically updates theRowKeyproperty in the same way.After updating the
editedSubscribermodel object, the code then determines whether the partition key or row key was changed. If either key value changed, the existing subscriber row has to be deleted and a new one inserted. If only the row key changed, the deletion and insertion can be done in an atomic batch transaction.Notice that the code creates a new entity to pass in to the
Deleteoperation:// RowKey changed, do delete/insert in a batch.var batchOperation =newTableBatchOperation();
batchOperation.Delete(newSubscriber{PartitionKey= partitionKey,RowKey= rowKey,ETag= editedSubscriber.ETag});
batchOperation.Insert(editedSubscriber);
mailingListTable.ExecuteBatch(batchOperation);Entities that you pass in to operations in a batch must be distinct entities. For example, you can't create a
Subscriberentity, pass it in to aDeleteoperation, then change a value in the sameSubscriberentity and pass it in to anInsertoperation. If you did that, the state of the entity after the property change would be in effect for both the Delete and the Insert operation.Note: Operations in a batch must all be on the same partition. Because a change to the list name changes the partition key, it can't be done in a transaction.
Add the Subscriber MVC views
In Solution Explorer, create a new folder under the Views folder in the MVC project, and name it Subscriber.
Right-click the new Views\Subscriber folder, and choose Add Existing Item.
Navigate to the folder where you downloaded the sample application, select all five of the .cshtml files in the Views\Subscriber folder, and click Add.
Open the Edit.cshtml file and examine the code.
@modelMvcWebRole.Models.Subscriber@{ViewBag.Title="Edit Subscriber";}<h2>EditSubscriber</h2> @using (Html.BeginForm()) {
@Html.AntiForgeryToken()
@Html.ValidationSummary(true)
@Html.HiddenFor(model => model.SubscriberGUID)
@Html.HiddenFor(model => model.ETag)
<fieldset>
<legend>Subscriber</legend><div class="display-label">@Html.DisplayNameFor(model => model.ListName)</div>
<div class="editor-field">
@Html.DropDownList("ListName", String.Empty)
@Html.ValidationMessageFor(model => model.ListName)
</div><div class="editor-label">@Html.LabelFor(model => model.EmailAddress)</div>
<div class="editor-field">
@Html.EditorFor(model => model.EmailAddress)
@Html.ValidationMessageFor(model => model.EmailAddress)
</div><div class="editor-label">@Html.LabelFor(model => model.Verified)</div>
<div class="display-field">
@Html.EditorFor(model => model.Verified)
</div><p><input type="submit" value="Save"/></p>
</fieldset>}<div>@Html.ActionLink("Back to List","Index")</div> @section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}This code is similar to what you saw earlier for the
MailingListEdit view. TheSubscriberGUIDvalue is not shown, so the value is not automatically provided in a form field for theHttpPostcontroller method. Therefore, a hidden field is included in order to preserve this value.The other views contain code that is similar to what you already saw for the
MailingListcontroller.
Test the application
Run the project by pressing CTRL+F5, and then click Subscribers.

Use the Create function to add some mailing lists, and try the Edit and Delete functions to make sure they work.

MessageCreate and test the Message controller and views
The Message web UI is used by administrators to create, edit, and display information about messages that are scheduled to be sent to mailing lists.
Add the Message entity class to the Models folder
The Message entity class is used for the rows in the Message table that contain information about a message that is scheduled to be sent to a list. These rows include information such as the subject line, the list to send a message to, and the scheduled date to send it.
In Solution Explorer, right-click the Models folder in the MVC project, and choose Add Existing Item.
Navigate to the folder where you downloaded the sample application, select the Message.cs file in the Models folder, and click Add.
Open Message.cs and examine the code.
publicclassMessage:TableEntity{privateDateTime? _scheduledDate;privatelong _messageRef;publicMessage(){this.MessageRef=DateTime.Now.Ticks;this.Status="Pending";}[Required][Display(Name="Scheduled Date")]// DataType.Date shows Date only (not time) and allows easy hook-up of jQuery DatePicker[DataType(DataType.Date)]publicDateTime?ScheduledDate{get{return _scheduledDate;}set{
_scheduledDate = value;this.PartitionKey= value.Value.ToString("yyyy-MM-dd");}}publiclongMessageRef{get{return _messageRef;}set{
_messageRef = value;this.RowKey="message"+ value.ToString();}}[Required][Display(Name="List Name")]publicstringListName{get;set;}[Required][Display(Name="Subject Line")]publicstringSubjectLine{get;set;}// Pending, Queuing, Processing, CompletepublicstringStatus{get;set;}}The
Messageclass defines a default constructor that sets theMessageRefproperty to a unique value for the message. Since this value is part of the row key, the setter for theMessageRefproperty automatically sets theRowKeyproperty also. TheMessageRefproperty setter concatenates the "message" literal and theMessageRefvalue and puts that in theRowKeyproperty.The
MessageRefvalue is created by getting theTicksvalue fromDateTime.Now. This ensures that by default when displaying messages in the web UI they will be displayed in the order in which they were created for a given scheduled date (ScheduledDateis the partition key). You could use a GUID to make message rows unique, but then the default retrieval order would be random.The default constructor also sets default status of Pending for new
messagerows.For more information about the
Messagetable structure, see the first tutorial in the series.
Add the Message MVC controller
In Solution Explorer, right-click the Controllers folder in the MVC project, and choose Add Existing Item.
Navigate to the folder where you downloaded the sample application, select the MessageController.cs file in the Controllers folder, and click Add.
Open MessageController.cs and examine the code.
Most of the code in this controller is similar to what you saw in the
Subscribercontroller. What is new here is code for working with blobs. For each message, the HTML and plain text content of the email is uploaded in the form of .htm and .txt files and stored in blobs.Blobs are stored in blob containers. The Windows Azure Email Service application stores all of its blobs in a single blob container named "azuremailblobcontainer", and code in the controller constructor gets a reference to this blob container:
publicclassMessageController:Controller{privateTableServiceContext serviceContext;privatestaticCloudBlobContainer blobContainer;publicMessageController(){var storageAccount =CloudStorageAccount.Parse(RoleEnvironment.GetConfigurationSettingValue("StorageConnectionString"));// If this is running in a Windows Azure Web Site (not a Cloud Service) use the Web.config file:// var storageAccount = CloudStorageAccount.Parse(ConfigurationManager.ConnectionStrings["StorageConnectionString"].ConnectionString);// Get context object for working with tables and a reference to the blob container.var tableClient = storageAccount.CreateCloudTableClient();
serviceContext = tableClient.GetTableServiceContext();var blobClient = storageAccount.CreateCloudBlobClient();
blobContainer = blobClient.GetContainerReference("azuremailblobcontainer");}For each file that a user selects to upload, the MVC view provides an
HttpPostedFileobject that contains information about the file. When the user creates a new message, theHttpPostedFileobject is used to save the file to a blob. When the user edits a message, the user can choose to upload a replacement file or leave the blob unchanged.The controller includes a method that the
HttpPost CreateandHttpPost Editmethods call to save a blob:privatevoidSaveBlob(string blobName,HttpPostedFileBase httpPostedFile){// Retrieve reference to a blob. CloudBlockBlob blob = blobContainer.GetBlockBlobReference(blobName);// Create the blob or overwrite the existing blob by uploading a local file.using(var fileStream = httpPostedFile.InputStream){
blob.UploadFromStream(fileStream);}}The
HttpPost Createmethod saves the two blobs and then adds theMessagetable row. Blobs are named by concatenating theMessageRefvalue with the file name extension ".htm" or ".txt".[HttpPost][ValidateAntiForgeryToken]publicActionResultCreate(Message message,HttpPostedFileBase file,HttpPostedFileBase txtFile){if(file ==null){ModelState.AddModelError(string.Empty,"Please provide an HTML file path");}if(txtFile ==null){ModelState.AddModelError(string.Empty,"Please provide a Text file path");}if(ModelState.IsValid){SaveBlob(message.MessageRef+".htm", file);SaveBlob(message.MessageRef+".txt", txtFile);var insertOperation =TableOperation.Insert(message);
messageTable.Execute(insertOperation);returnRedirectToAction("Index");}var lists =GetListNames();ViewBag.ListName=newSelectList(lists,"ListName","Description");returnView(message);}The
HttpGet Editmethod validates that the retrieved message is inPendingstatus so that the user can't change a message once worker role B has begun processing it. Similar code is in theHttpPost Editmethod and theDeleteandDeleteConfirmedmethods.publicActionResultEdit(string partitionKey,string rowKey){var message =FindRow(partitionKey, rowKey);if(message.Status!="Pending"){thrownewException("Message can't be edited because it isn't in Pending status.");}var lists =GetListNames();ViewBag.ListName=newSelectList(lists,"ListName","Description", message.ListName);returnView(message);}In the
HttpPost Editmethod, the code saves a new blob only if the user chose to upload a new file. The following code omits the concurrency handling part of the method, which is the same as what you saw earlier for theMailingListcontroller.[HttpPost][ValidateAntiForgeryToken]publicActionResultEdit(string partitionKey,string rowKey,Message editedMsg,DateTime scheduledDate,HttpPostedFileBase httpFile,HttpPostedFileBase txtFile){if(ModelState.IsValid){var excludePropLst =newList<string>();
excludePropLst.Add("PartitionKey");
excludePropLst.Add("RowKey");if(httpFile ==null){// They didn't enter a path or navigate to a file, so don't update the file.
excludePropLst.Add("HtmlPath");}else{// They DID enter a path or navigate to a file, assume it's changed.SaveBlob(editedMsg.MessageRef+".htm", httpFile);}if(txtFile ==null){
excludePropLst.Add("TextPath");}else{SaveBlob(editedMsg.MessageRef+".txt", txtFile);}string[] excludeProperties = excludePropLst.ToArray();try{UpdateModel(editedMsg,string.Empty,null, excludeProperties);if(editedMsg.PartitionKey== partitionKey){// Keys didn't change -- update the row.var replaceOperation =TableOperation.Replace(editedMsg);
messageTable.Execute(replaceOperation);}else{// Partition key changed -- delete and insert the row.// (Partition key has scheduled date which may be changed;// row key has MessageRef which does not change.)var deleteOperation =TableOperation.Delete(newMessage{PartitionKey= partitionKey,RowKey= rowKey,ETag= editedMsg.ETag});
messageTable.Execute(deleteOperation);var insertOperation =TableOperation.Insert(editedMsg);
messageTable.Execute(insertOperation);}returnRedirectToAction("Index");}If the scheduled date is changed, the partition key is changed, and a row has to be deleted and inserted. This can't be done in a transaction because it affects more than one partition.
The
HttpPost Deletemethod deletes the blobs when it deletes the row in the table:[HttpPost,ActionName("Delete")]publicActionResultDeleteConfirmed(String partitionKey,string rowKey){// Get the row again to make sure it's still in Pending status.var message =FindRow(partitionKey, rowKey);if(message.Status!="Pending"){thrownewException("Message can't be deleted because it isn't in Pending status.");}DeleteBlob(message.MessageRef+".htm");DeleteBlob(message.MessageRef+".txt");var deleteOperation =TableOperation.Delete(message);
messageTable.Execute(deleteOperation);returnRedirectToAction("Index");}privatevoidDeleteBlob(string blobName){var blob = blobContainer.GetBlockBlobReference(blobName);
blob.Delete();}
Add the Message MVC views
In Solution Explorer, create a new folder under the Views folder in the MVC project, and name it
Message.Right-click the new Views\Message folder, and choose Add Existing Item.
Navigate to the folder where you downloaded the sample application, select all five of the .cshtml files in the Views\Message folder, and click Add.
Open the Edit.cshtml file and examine the code.
@modelMvcWebRole.Models.Message@{ViewBag.Title="Edit Message";}<h2>EditMessage</h2> @using (Html.BeginForm("Edit", "Message", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
@Html.AntiForgeryToken()
@Html.ValidationSummary(true)
@Html.HiddenFor(model => model.ETag)
<fieldset>
<legend>Message</legend>
@Html.HiddenFor(model => model.MessageRef)
@Html.HiddenFor(model => model.PartitionKey)
@Html.HiddenFor(model => model.RowKey)
<div class="editor-label">
@Html.LabelFor(model => model.ListName, "MailingList")
</div>
<div class="editor-field">
@Html.DropDownList("ListName", String.Empty)
@Html.ValidationMessageFor(model => model.ListName)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.SubjectLine)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.SubjectLine)
@Html.ValidationMessageFor(model => model.SubjectLine)
</div>
<div class="editor-label">
HTML Path: Leave blank to keep current HTML File.
</div>
<div class="editor-field">
<input type="file" name="file" />
</div>
<div class="editor-label">
Text Path: Leave blank to keep current Text File.
</div>
<div class="editor-field">
<input type="file" name="TxtFile" />
</div>
<div class="editor-label">
@Html.LabelFor(model => model.ScheduledDate)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.ScheduledDate)
@Html.ValidationMessageFor(model => model.ScheduledDate)
</div>
<div class="display-label">
@Html.DisplayNameFor(model => model.Status)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Status)
</div>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
} <div>
@Html.ActionLink("Back to List", "Index")
</div> @section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}The
HttpPost Editmethod needs the partition key and row key, so the code provides these in hidden fields. The hidden fields were not needed in theSubscribercontroller because (a) theListNameandEmailAddressproperties in theSubscribermodel update thePartitionKeyandRowKeyproperties, and (b) theListNameandEmailAddressproperties were included withEditorForhelpers in the Edit view. When the MVC model binder for theSubscribermodel updates theListNameproperty, thePartitionKeyproperty is automatically updated, and when the MVC model binder updates theEmailAddressproperty in theSubscribermodel, theRowKeyproperty is automatically updated. In theMessagemodel, the fields that map to partition key and row key are not editable fields, so they don't get set that way.A hidden field is also included for the
MessageRefproperty. This is the same value as the partition key, but it is included in order to enable better code clarity in theHttpPost Editmethod. Including theMessageRefhidden field enables the code in theHttpPost Editmethod to refer to theMessageRefvalue by that name when it constructs file names for the blobs.Open the Index.cshtml file and examine the code.
@modelIEnumerable<MvcWebRole.Models.Message>@{ViewBag.Title="Messages";}<h2>Messages</h2> <p>
@Html.ActionLink("Create New", "Create")
</p><table><tr><th>@Html.DisplayNameFor(model => model.ListName)</th>
<th>
@Html.DisplayNameFor(model => model.SubjectLine)
</th><th>@Html.DisplayNameFor(model => model.ScheduledDate)</th>
<th>
@Html.DisplayNameFor(model => model.Status)
</th><th></th>
</tr>@foreach(var item inModel){<tr><td>@Html.DisplayFor(modelItem => item.ListName)</td>
<td>
@Html.DisplayFor(modelItem => item.SubjectLine)
</td><td>@Html.DisplayFor(modelItem => item.ScheduledDate)</td>
<td>
@item.Status
</td><td>@if(item.Status=="Pending"){@Html.ActionLink("Edit","Edit",new{PartitionKey= item.PartitionKey,RowKey= item.RowKey})@:|@Html.ActionLink("Delete","Delete",new{PartitionKey= item.PartitionKey,RowKey= item.RowKey})@:|}@Html.ActionLink("Details","Details",new{PartitionKey= item.PartitionKey,RowKey= item.RowKey})</td>
</tr>}</table>A difference here from the other Index views is that the Edit and Delete links are shown only for messages that are in
Pendingstatus:@if(item.Status=="Pending"){@Html.ActionLink("Edit","Edit",new{PartitionKey= item.PartitionKey,RowKey= item.RowKey})@:|@Html.ActionLink("Delete","Delete",new{PartitionKey= item.PartitionKey,RowKey= item.RowKey})@:|}This helps prevent the user from making changes to a message after worker role A has begun to process it.
The other views contain code that is similar to the Edit view or the other views you saw for the other controllers.
Test the application
Run the project by pressing CTRL+F5, then click Messages.

Use the Create function to add some mailing lists, and try the Edit and Delete functions to make sure they work.

UnsubscribeCreate and test the Unsubscribe controller and view
Next, you'll implement the UI for the unsubscribe process.
Note: This tutorial only builds the controller for the unsubscribe process, not the subscribe process. As was explained in the first tutorial, the UI and service method for the subscription process have been left out until we implement appropriate security for the service method. Until then, you can use the Subscriber administrator pages to subscribe email addresses to lists.
Add the Unsubscribe view model to the Models folder
The UnsubscribeVM view model is used to pass data between the Unsubscribe controller and its view.
In Solution Explorer, right-click the
Modelsfolder in the MVC project, and choose Add Existing Item.Navigate to the folder where you downloaded the sample application, select the
UnsubscribeVM.csfile in the Models folder, and click Add.Open
UnsubscribeVM.csand examine the code.publicclassUnsubscribeVM{publicstringEmailAddress{get;set;}publicstringListName{get;set;}publicstringListDescription{get;set;}publicstringSubscriberGUID{get;set;}publicbool?Confirmed{get;set;}}Unsubscribe links contain the
SubscriberGUID. That value is used to get the email address, list name, and list description from theMailingListtable. The view displays the email address and the description of the list that is to be unsubscribed from, and it displays a Confirm button that the user must click to complete the unsubscription process.
Add the Unsubscribe controller
In Solution Explorer, right-click the
Controllersfolder in the MVC project, and choose Add Existing Item.Navigate to the folder where you downloaded the sample application, select the UnsubscribeController.cs file in the Controllers folder, and click Add.
Open UnsubscribeController.cs and examine the code.
This controller has an
HttpGet Indexmethod that displays the initial unsubscribe page, and anHttpPost Indexmethod that processes the Confirm or Cancel button.The
HttpGet Indexmethod uses the GUID and list name in the query string to get theMailingListtable row for the subscriber. Then it puts all the information needed by the view into the view model and displays the Unsubscribe page. It sets theConfirmedproperty to null in order to tell the view to display the initial version of the Unsubscribe page.publicActionResultIndex(string id,string listName){if(string.IsNullOrEmpty(id)==true||string.IsNullOrEmpty(listName)){ViewBag.errorMessage ="Empty subscriber ID or list name.";returnView("Error");}string filter =TableQuery.CombineFilters(TableQuery.GenerateFilterCondition("PartitionKey",QueryComparisons.Equal, listName),TableOperators.And,TableQuery.GenerateFilterCondition("SubscriberGUID",QueryComparisons.Equal, id));var query =newTableQuery<Subscriber>().Where(filter);var subscriber = mailingListTable.ExecuteQuery(query).ToList().Single();if(subscriber ==null){ViewBag.Message="You are already unsubscribed";returnView("Message");}var unsubscribeVM =newUnsubscribeVM();
unsubscribeVM.EmailAddress=MaskEmail(subscriber.EmailAddress);
unsubscribeVM.ListDescription=FindRow(subscriber.ListName,"mailinglist").Description;
unsubscribeVM.SubscriberGUID= id;
unsubscribeVM.Confirmed=null;returnView(unsubscribeVM);}Note: The SubscriberGUID is not in the partition key or row key, so the performance of this query will degrade as partition size (the number of email addresses in a mailing list) increases. For more information about alternatives to make this query more scalable, see the first tutorial in the series.
The
HttpPost Indexmethod again uses the GUID and list name to get the subscriber information and populates the view model properties. Then, if the Confirm button was clicked, it deletes the subscriber row in theMailingListtable. If the Confirm button was pressed it also sets theConfirmproperty totrue, otherwise it sets theConfirmproperty tofalse. The value of theConfirmproperty is what tells the view to display the confirmed or canceled version of the Unsubscribe page.[HttpPost][ValidateAntiForgeryToken]publicActionResultIndex(string subscriberGUID,string listName,string action){string filter =TableQuery.CombineFilters(TableQuery.GenerateFilterCondition("PartitionKey",QueryComparisons.Equal, listName),TableOperators.And,TableQuery.GenerateFilterCondition("SubscriberGUID",QueryComparisons.Equal, subscriberGUID));var query =newTableQuery<Subscriber>().Where(filter);var subscriber = mailingListTable.ExecuteQuery(query).ToList().Single();var unsubscribeVM =newUnsubscribeVM();
unsubscribeVM.EmailAddress=MaskEmail(subscriber.EmailAddress);
unsubscribeVM.ListDescription=FindRow(subscriber.ListName,"mailinglist").Description;
unsubscribeVM.SubscriberGUID= subscriberGUID;
unsubscribeVM.Confirmed=false;if(action =="Confirm"){
unsubscribeVM.Confirmed=true;var deleteOperation =TableOperation.Delete(subscriber);
mailingListTable.Execute(deleteOperation);}returnView(unsubscribeVM);}
Create the MVC views
In Solution Explorer, create a new folder under the Views folder in the MVC project, and name it Unsubscribe.
Right-click the new Views\Unsubscribe folder, and choose Add Existing Item.
Navigate to the folder where you downloaded the sample application, select the Index.cshtml file in the Views\Unsubscribe folder, and click Add.
Open the Index.cshtml file and examine the code.
@modelMvcWebRole.Models.UnsubscribeVM@{ViewBag.Title="Unsubscribe";Layout=null;}<h2>EmailListSubscriptionService</h2> @using (Html.BeginForm()) {
@Html.AntiForgeryToken()
@Html.ValidationSummary(true)
<fieldset>
<legend>Unsubscribe from Mailing List</legend>@Html.HiddenFor(model => model.SubscriberGUID)@Html.HiddenFor(model => model.EmailAddress)@Html.HiddenFor(model => model.ListName)@if(Model.Confirmed==null){<p>Do you want to unsubscribe @Html.DisplayFor(model => model.EmailAddress)from:@Html.DisplayFor(model => model.ListDescription)?</p>
<br /><p><input type="submit" value="Confirm" name="action"/>  <input type="submit" value="Cancel" name="action"/></p>
}
@if (Model.Confirmed == false) {
<p>
@Html.DisplayFor(model => model.EmailAddress) will NOT be unsubscribed from: @Html.DisplayFor(model => model.ListDescription).
</p>}@if(Model.Confirmed==true){<p>@Html.DisplayFor(model => model.EmailAddress) has been unsubscribed from:@Html.DisplayFor(model => model.ListDescription).</p>
}
</fieldset>}@sectionScripts{@Scripts.Render("~/bundles/jqueryval")}The
Layout = nullline specifies that the _Layout.cshtml file should not be used to display this page. The Unsubscribe page displays a very simple UI without the headers and footers that are used for the administrator pages.In the body of the page, the
Confirmedproperty determines what will be displayed on the page: Confirm and Cancel buttons if the property is null, unsubscribe-confirmed message if the property is true, unsubscribe-canceled message if the property is false.
Test the application
Run the project by pressing CTRL-F5, and then click Subscribers.
Click Create and create a new subscriber for any mailing list that you created when you were testing earlier.
Leave the browser window open on the SubscribersIndex page.
Open Azure Storage Explorer, and then select your test storage account.
Click Tables under Storage Type, select the MailingList table, and then click Query.
Double-click the subscriber row that you added.

In the Edit Entity dialog box, select and copy the
SubscriberGUIDvalue.
Switch back to your browser window. In the address bar of the browser, change "Subscriber" in the URL to "unsubscribe?ID=[guidvalue]&listName=[listname]" where [guidvalue] is the GUID that you copied from Azure Storage Explorer, and [listname] is the name of the mailing list. For example:
http://127.0.0.1/unsubscribe?ID=b7860242-7c2f-48fb-9d27-d18908ddc9aa&listName=contoso1
The version of the Unsubscribe page that asks for confirmation is displayed:

Click Confirm and you see confirmation that the email address has been unsubscribed.

Go back to the SubscribersIndex page to verify that the subscriber row is no longer there.
Alternative Architecture(Optional) Build the Alternative Architecture
The following changes to the instructions apply if you want to build the alternative architecture -- that is, running the web UI in a Windows Azure Web Site instead of a Windows Azure Cloud Service web role.
When you create the solution, create the ASP.NET MVC 4 Web Application project first, and then add to the solution a Windows Azure Cloud Service project with a worker role.
Store the Windows Azure Storage connection string in the Web.config file instead of the cloud service settings file. (This only works for Windows Azure Web Sites. If you try to use the Web.config file for the storage connection string in a Windows Azure Cloud Service web role, you'll get an HTTP 500 error.)
Add a new connection string named
StorageConnectionStringto the Web.config file, as shown in the following example:<connectionStrings><addname="DefaultConnection"connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=aspnet-MvcWebRole-20121010185535;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\aspnet-MvcWebRole-20121010185535.mdf"providerName="System.Data.SqlClient"/><addname="StorageConnectionString"connectionString="DefaultEndpointsProtocol=https;AccountName=[accountname];AccountKey=[primarykey]"/></connectionStrings>
Get the values for the connection string from the Windows Azure management portal: select the Storage tab and your storage account, and then click Manage keys at the bottom of the page.
Wherever you see
RoleEnvironment.GetConfigurationSettingValue("StorageConnectionString")in the code, replace it withConfigurationManager.ConnectionStrings["StorageConnectionString"].ConnectionString.
Next stepsNext steps
As explained in the first tutorial in the series, we are not showing how to build the subscribe process in detail in this tutorial until we implement a shared secret to secure the ASP.NET Web API service method. However, the IP restriction also protects the service method and you can add the subscribe functionality by copying the following files from the downloaded project.
For the ASP.NET Web API service method:
- Controllers\SubscribeAPI.cs
 
For the web page that subscribers get when they click on the Confirm link in the email that is generated by the service method:
- Models\SubscribeVM.cs
 - Controllers\SubscribeController.cs
 - Views\Subscribe\Index.cshtml
 
In the next tutorial you'll configure and program worker role A, the worker role that schedules emails.
For links to additional resources for working with Windows Azure Storage tables, queues, and blobs, see the end of the last tutorial in this series.
[Windows Azure] Building the web role for the Windows Azure Email Service application - 3 of 5的更多相关文章
- [Windows Azure] Building worker role B (email sender) for the Windows Azure Email Service application - 5 of 5.
		
Building worker role B (email sender) for the Windows Azure Email Service application - 5 of 5. This ...
 - [Windows Azure] Building worker role A (email scheduler) for the Windows Azure Email Service application - 4 of 5.
		
Building worker role A (email scheduler) for the Windows Azure Email Service application - 4 of 5. T ...
 - [Windows Azure] Configuring and Deploying the Windows Azure Email Service application - 2 of 5
		
Configuring and Deploying the Windows Azure Email Service application - 2 of 5 This is the second tu ...
 - Windows Azure Cloud Service (11) PaaS之Web Role, Worker Role(上)
		
<Windows Azure Platform 系列文章目录> 本文是对Windows Azure Platform (六) Windows Azure应用程序运行环境内容的补充. 我们知 ...
 - Windows Azure Cloud Service (12) PaaS之Web Role, Worker Role, Azure Storage Queue(下)
		
<Windows Azure Platform 系列文章目录> 本章DEMO部分源代码,请在这里下载. 在上一章中,笔者介绍了我们可以使用Azure PaaS的Web Role和Worke ...
 - 【Azure 云服务】Azure Cloud Service 为 Web Role(IIS Host)增加自定义字段 (把HTTP Request Header中的User-Agent字段增加到IIS输出日志中)
		
问题描述 把Web Role服务发布到Azure Cloud Service后,需要在IIS的输出日志中,把每一个请求的HTTP Request Header中的User-Agent内容也输出到日志中 ...
 - Windows Azure Web Role 的 IIS 重置
		
 如果您是一名 Web开发人员,您很可能使用过"简单快捷"的iisreset命令重置运行不正常的 IIS主机.这种方法通常在经典的 Windows Server VM上非常有效 ...
 - Windows Azure入门教学系列 (二):部署第一个Web Role程序
		
本文是Windows Azure入门教学的第二篇文章. 在第一篇教学中,我们已经创建了第一个Web Role程序.在这篇教学中,我们将学习如何把该Web Role程序部署到云端. 注意:您需要购买Wi ...
 - Azure web role, work role 以及其他role
		
Azure web role, work role 以及其他role 如果没有创建过web role 和work role的话可以参考如下文章来创建一下web role 和work role. htt ...
 
随机推荐
- RHEL SHELL快捷键
			
Shell快捷键 CTRL+a 调到命令行头 e 调到命令行尾 CTRL+u 光标前面的删除 k 光标后面的删除 CTRL+→词的头 词的尾 ESC+. 粘贴上个命令的尾词 杀掉远 ...
 - Android网络开发之蓝牙
			
蓝牙采用分散式网络结构以及快调频和短包技术,支持点对点及点对多点通信,工作在全球通用的2.4GHz ISM(I-工业.S-科学.M-医学)频段,其数据速率为1Mbps,采用时分双工传输方案. 蓝牙 ...
 - webview中事件的用法
			
封装 MBProgressHud ==================================== #import "MBProgressHUD.h" @interface ...
 - <转>lua解释执行脚本流程
			
本文转自:http://www.cnblogs.com/zxh1210603696/p/4458473.html #include "lua.hpp" #include <i ...
 - [Spring学习笔记 5 ] Spring AOP 详解1
			
知识点回顾:一.IOC容器---DI依赖注入:setter注入(属性注入)/构造子注入/字段注入(注解 )/接口注入 out Spring IOC容器的使用: A.完全使用XML文件来配置容器所要管理 ...
 - 使用btrace来找出执行慢的方法
			
转载于:https://shaojun.name/2016/07/260 btrace script import static com.sun.btrace.BTraceUtils.name; im ...
 - Zabbix Trigger表达式实例
			
Zabbix提供强大的触发器(Trigger)函数以方便进行更为灵活的报警及后续动作,具体触发器函数可以访问https://www.zabbix.com/documentation/2.0/manua ...
 - js跨域问题解释 使用jsonp或jQuery的解决方案
			
js跨域及解决方案 1.什么是跨域 我们经常会在页面上使用ajax请求访问其他服务器的数据,此时,客户端会出现跨域问题. 跨域问题是由于javascript语言安全限制中的同源策略造成的. 简单来说, ...
 - 基于24位AD转换模块HX711的重量称量实验(已补充皮重存储,线性温度漂移修正)
			
转载:http://www.geek-workshop.com/thread-2315-1-1.html 以前在X宝上买过一个称重放大器,180+大洋.原理基本上就是把桥式拉力传感器输出的mV级信号放 ...
 - 【java】break outer,continue outer的使用
			
break默认是结束当前循环,有时我们在使用循环时,想通过内层循环里的语句直接跳出外层循环,java提供了使用break直接跳出外层循环,此时需要在break后通过标签指定外层循环.java中的标签是 ...