Chapter 6: From 2005 to 2010: Designing the Look and Feel


This chapter is taken from book "Moving to Microsoft Visual Studio 2010" by Patrice Pelland, Pascel Pare, and Ken Haines published for Microsoft Press.

After reading this chapter, you will be able to

  • Create an ASP.NET MVC controller that interacts with the data model
  • Create an ASP.NET MVC view that displays data from the controller and validates user input
  • Extend the application with an external plug-in using the Managed Extensibility Framework

Web application development in Microsoft Visual Studio has certainly made significant improvements over the years since ASP.NET 1.0 was released. Visual Studio 2005 and .NET Framework 2.0 included things such as more efficient view state, partial classes, and generic types (plus many others) to help developers create efficient applications that were easy to manage.

The spirit of improvement to assist developers in creating world-class applications is very much alive in Visual Studio 2010. In this chapter, we'll explore some of the new features as we add functionality to the Plan My Night companion application.

Note The companion application is an ASP.NET MVC 2 project, but a Web developer has a choice in Visual Studio 2010 to use this new form of ASP.NET application or the more traditional ASP.NET (referred to in the community as Web Forms for distinction). ASP.NET 4.0 has many improvements to help developers and is still a very viable approach to creating Web applications.

We'll be using a modified version of the companion application's solution to work our way through this chapter. If you installed the companion content in the default location, the correct solution can be found at Documents\Microsoft Press\Moving to Visual Studio 2010\Chapter 6\ in a folder called UserInterface-Start.

Introducing the PlanMyNight.Web Project

The user interface portion of Plan My Night in Visual Studio 2010 was developed as an ASP.NET MVC application, the layout of which differs from what a developer might be accustomed to when developing an ASP.NET Web Forms application in Visual Studio 2005. Some items in the project (as seen in Figure 6-1) will look familiar (such as Global.asax), but others are completely new, and some of the structure is required by the ASP.NET MVC framework.

Figure-6-1.gif

FIGURE 6-1 PlanMyNight.Web project view

Here are the items required by ASP.NET MVC:

  • Areas This folder is used by the ASP.NET MVC framework to organize large Web applications into smaller components, without using separate solutions or projects. This feature is not used in the Plan My Night application but is called out because this folder is created by the MVC project template.
  • Controllers During request processing, the ASP.NET MVC framework looks for controllers in this folder to handle the request.
  • Views The Views folder is actually a structure of folders. The layer immediately inside the Views folder is named for each of the classes found in the Controllers folder, plus a Shared folder. The Shared subfolder is for common views, partial views, master pages, and anything else that will be available to all controllers.

See Also More information about ASP.NET MVC components, as well as how its request processing differs from ASP.NET Web Forms, can be found at http://asp.net/mvc.

In most cases, the web.config file is the last file in a project's root folder. However, it has received a much-needed update in Visual Studio 2010: Web.config Transformation. This feature allows for a base web.config file to be created but then to have build-specific web.config files override the settings of the base at build, deployment, and run times. These files appear under the base web.config file, as seen in Figure 6-2.

Figure-6-2.gif

FIGURE 6-2 A web.config file with build-specific files expanded

Visual Studio 2005 When working on a project in Visual Studio 2005, do you recall needing to remember not to overwrite the web.config file with your debug settings? Or needing to remember to update web.config when it was published for a retail build with the correct settings? This is no longer an issue in Visual Studio 2010. The settings in the web.Release.config file will be used during release builds to override the values in web.config, and the same goes for web.Debug.config in debug builds.

Other sections of the project include the following:

  • Content A collection of folders containing images, scripts, and style files
  • Helpers Includes miscellaneous classes, containing a number of extension methods, that add functionality to types used in the project
  • Infrastructure Contains items related to dealing with the lower level infrastructure of ASP.NET MVC (for example, caching and controller factories)
  • ViewModels Contains data entities filled out by controller classes and used by views to display data

Running the Project

If you compile and run the project, you should see a screen similar to Figure 6-3.

Figure-6-3.gif

FIGURE 6-3 Default page of the Plan My Night application

The searching functionality and the ability to organize an initial list of itinerary items all work, but if you attempt to save the itinerary you are working on, or if you log in with Windows Live ID, the application will return a 404 Not Found error screen (as shown in Figure 6-4).

Figure-6-4.gif

FIGURE 6-4 Error screen returned when logging in to the Plan My Night application

You get this error message because currently the project does not include an account controller to handle these requests.

Creating the Account Controller

The AccountController class provides some critical functionality to the companion Plan My Night application:

  • It handles signing users in and out of the application (via Windows Live ID).
  • It provides actions for displaying and updating user profile information.

To create a new ASP.NET MVC controller:

  1. Use Solution Explorer to navigate to the Controllers folder in the PlanMyNight.Web project, and click the right mouse button.
  2. Open the Add submenu, and select the Controller item.

    Figure-6-4.1.gif
     
  3. Fill in the name of the controller as AccountController.

    Figure-6-4.2.gif

Note Leave the Add Action Methods For Create, Update, And Delete Scenarios check box blank.

Selecting the box inserts some "starter" action methods, but because you will not be using the default methods, there is no reason to create them.

After you click the Add button in the Add Controller dialog box, you should have a basic AccountController class open, with a single Index method in its body:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace Microsoft.Samples.PlanMyNight.Web.Controllers
{
    public class AccountController : Controller
    {
        //
        // GET: /Account/
        public ActionResult Index()
        {
            return View();
        }
    }
}

Visual Studio 2005 A difference to be noted from developing ASP.NET Web Forms applications in Visual Studio 2005 is that ASP.NET MVC applications do not have a companion code-behind file for each of their .aspx files. Controllers like the one you are currently creating perform the logic required to process input and prepare output. This approach allows for a clear separation of display and business logic, and it's a key aspect of ASP.NET MVC.

Implementing the Functionality

To communicate with any of the data layers and services (the Model), you'll need to add some instance fields and initialize them. Before that, you need to add some namespaces to your using block:

using System.IO;
using Microsoft.Samples.PlanMyNight.Data;
using Microsoft.Samples.PlanMyNight.Entities;
using Microsoft.Samples.PlanMyNight.Infrastructure;
using Microsoft.Samples.PlanMyNight.Infrastructure.Mvc;
using Microsoft.Samples.PlanMyNight.Web.ViewModels;
using System.Collections.Specialized;
using WindowsLiveId;

Now, let's add the instance fields. These fields are interfaces to the various sections of your Model:

public class AccountController : Controller
{
private readonlyI WindowsLiveLogin windowsLogin;
private readonlyI MembershipService membershipService;
private readonlyI FormsAuthentication formsAuthentication;
private readonlyI ReferenceRepository referenceRepository;
private readonlyI ActivitiesRepository activitiesRepository;
}

Note Using interfaces to interact with all external dependencies allows for better portability of the code to various platforms. Also, during testing, dependencies can be mimicked much easier when using interfaces, making for more efficient isolation of a specific component.

As mentioned, these fields represent parts of the Model this controller will interact with to meet its functional needs. Here are the general descriptions for each of the interfaces:

  • IWindowsLiveLogin Provides functionality for interacting with the Windows Live ID service.
  • IMembershipService Provides user profile information and authorization methods. In your companion application, it is an abstraction of the ASP.NET Membership Service.
  • IFormsAuthentication Provides for ASP.NET Forms Authentication abstraction.
  • IReferenceRepository Provides reference resources, such as lists of states and other model-specific information.
  • IActivitiesRepository An interface for retrieving and updating activity information.

You'll add two constructors to this class: one for general run-time use, which uses the ServiceFactory class to get references to the needed interfaces, and one to enable tests to inject specific instances of the interfaces to use.

public AccountController() :
this(
new ServiceFactory().GetMembershipService(),
new WindowsLiveLogin(true),
new FormsAuthenticationService(),
new ServiceFactory().GetReferenceRepositoryInstance(),
new ServiceFactory().GetActivitiesRepositoryInstance())
{
}
publicAccountController(
IMembershipService membershipService,

IWindowsLiveLogin windowsLogin,
IFormsAuthentication formsAuthentication,
IReferenceRepository referenceRepository,
IActivitiesRepository activitiesRepository)
{
this.membershipService = membershipService;
this.windowsLogin = windowsLogin;
this.formsAuthentication = formsAuthentication;
this.referenceRepository = referenceRepository;
this.activitiesRepository = activitiesRepository;
}

Authenticating the User

The first real functionality you'll implement in this controller is that of signing in and out of the application. Most of the methods you'll implement later require authentication, so this is a good place to start.

The companion application uses a few technologies together at the same time to give the user a smooth authentication experience: Windows Live ID, ASP.NET Forms Authentication, and ASP.NET Membership Services. These three technologies are used in the LiveID action you'll implement next.

Start by creating the following method in the AccountController class:

        public ActionResult LiveId()
        {
            return Redirect("~/");
        }

This method will be the primary action invoked when interacting with the Windows Live ID services. Right now, if it is invoked, it will just redirect the user to the root of the application.

Note The call to Redirect returns RedirectResult, and although this example uses a string to define the target of the redirection, various overloads can be used for different situations.

A few different types of actions can be taken when Windows Live ID returns a user to your application. The user can be signing in to Windows Live ID, signing out, or clearing the Windows Live ID cookies. Windows Live ID uses a query string parameter called action on the URL when it returns a user, so you'll use a switch to branch the logic depending on the value of the parameter.

Add the following to the LiveId method above the return statement:

            string action = Request.QueryString["action"];
            switch (action)
            {
                case "logout":
                    this.formsAuthentication.SignOut();
                    return Redirect("~/");
                case "clearcookie":
                    this.formsAuthentication.SignOut();
                    string type;
                    byte[] content;
                    this.windowsLogin.GetClearCookieResponse(out type, out content);
                    return new FileStreamResult(new MemoryStream(content), type);
            }

See also Full documentation of the Windows Live ID system can be found on the http://dev.live.com/ Web site.

The code you just added handles the two sign-out actions for Windows Live ID. In both cases, you use the IFormsAuthentication interface to remove the ASP.NET Forms Authentication cookie so that any future http requests (until the user signs in again) will not be considered authenticated. In the second case, you went one step further to clear the Windows Live ID cookies (the ones that remember your login name but not your password).

Handling the sign-in scenario requires a bit more code because you have to check whether the authenticating user is in your Membership Database and, if not, create a profile for the user. However, before that, you must pass the data that Windows Live ID sent you to your Windows Live ID interface so that it can validate the information and give you a WindowsLiveLogin.User object:

            // login
            NameValueCollection tokenContext;
            if ((Request.HttpMethod ?? "GET").ToUpperInvariant() == "POST")
            {
                tokenContext = Request.Form;
            }
            else
            {
                tokenContext = new NameValueCollection(Request.QueryString);
                tokenContext["stoken"] =
                System.Web.HttpUtility.UrlEncode(tokenContext["stoken"]);
            }
            var liveIdUser = this.windowsLogin.ProcessLogin(tokenContext);

At this point in the case for logging in, either liveIdUser will be a reference to an authenticated WindowsLiveLogin.User object or it will be null. With this in mind, you can add your next section of the code, which takes action when the liveIdUser value is not null:

            if (liveIdUser != null)
            {
                var returnUrl = liveIdUser.Context;
                var userId = new Guid(liveIdUser.Id).ToString();
                if (!this.membershipService.ValidateUser(userId, userId))
                {
                    this.formsAuthentication.SignIn(userId, false);
                    this.membershipService.CreateUser(userId, userId, string.Empty);
                    var profile = this.membershipService.CreateProfile(userId);
                    profile.FullName = "New User";
                    profile.State = string.Empty;
                    profile.City = string.Empty;
                    profile.PreferredActivityTypeId = 0;
                    this.membershipService.UpdateProfile(profile);
                    if (string.IsNullOrEmpty(returnUrl)) returnUrl = null;
                    return RedirectToAction("Index", new { returnUrl = returnUrl });
                }
                else
                {
                    this.formsAuthentication.SignIn(userId, false);
                    if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/";
                    return Redirect(returnUrl);
                }
            }
            break;

The call to the ValidateUser method on the IMembershipService reference allows the application to check whether the user has been to this site before and whether there will be a profile for the user. Because the user is authenticated with Windows Live ID, you are using the user's ID value (which is a GUID) as both the user name and password to the ASP.NET Membership Service.

If the user does not have a user record with the application, you create one by calling the CreateUser method and then also create a user settings profile via CreateProfile. The profile is filled with some defaults and saved back to its store, and the user is redirected to the primary input page so that he can update the information.

Note Controller.RedirectToAction determines which URL to create based on the combination of input parameters. In this case, you want to redirect the user to the Index action of this controller, as well as pass the current return URL value.

The other action that takes place in this code is that the user is signed in to ASP.NET Forms authentication so that a cookie will be created, providing identity information on future requests that require authentication.

The settings profile is managed by ASP.NET Membership Services as well and is declared in the web.config file of the application:

<system.web>
 
      <
profile enabled="true">
            <properties>
                  <
add name="FullName" type="string" />
                  <add name="State" type="string" />
                  <add name="City" type="string" />
                  <add name="PreferredActivityTypeId" type="int" />
            </properties>
            <
providers>
                  <
clear />
                  <
add name="AspNetSqlProfileProvider"
                  type="System.Web.Profile.SqlProfileProvider,
System.Web, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a
"
                  connectionStringName="ApplicationServices"
                  applicationName="/" />
            </providers>
      </
profile>
 
</system.web>

At this point, the LiveID method is complete and should look like the following code. The application can now take authentication information from Windows Live ID, prepare an ASP.NET MembershipService profile, and create an ASP.NET Forms Authentication ticket.

public ActionResult LiveId()
{
string action = Request.QueryString["action"];
switch (action)
{
case "logout":
this.formsAuthentication.SignOut();
return Redirect("~/");
case "clearcookie":
this.formsAuthentication.SignOut();
string type;
byte[] content;
this.windowsLogin.GetClearCookieResponse(out type, out content);
return new FileStreamResult(new MemoryStream(content), type);
default:
// login
NameValueCollection tokenContext;
if ((Request.HttpMethod ?? "GET").ToUpperInvariant() == "POST")
{
tokenContext = Request.Form;
}
else
{
tokenContext = new NameValueCollection(Request.QueryString);
tokenContext["stoken"] =
System.Web.HttpUtility.UrlEncode(tokenContext["stoken"]);
}
var liveIdUser = this.windowsLogin.ProcessLogin(tokenContext);
if (liveIdUser != null)
{
var returnUrl = liveIdUser.Context;
var userId = new Guid(liveIdUser.Id).ToString();
if (!this.membershipService.ValidateUser(userId, userId))
{
this.formsAuthentication.SignIn(userId, false);
this.membershipService.CreateUser(userId, userId, string.Empty);
var profile = this.membershipService.CreateProfile(userId);
profile.FullName = "New User";
profile.State = string.Empty;
profile.City = string.Empty;
profile.PreferredActivityTypeId = 0;
this.membershipService.UpdateProfile(profile);
if (string.IsNullOrEmpty(returnUrl)) returnUrl = null;
return RedirectToAction("Index", new { returnUrl = returnUrl });
}
else
{
this.formsAuthentication.SignIn(userId, false);
if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/";
return Redirect(returnUrl);
}
}
break;
}
return Redirect("~/");
}

Of course, the user has to be able to get to the Windows Live ID login page in the first place before logging in. Currently in the Plan My Night application, there is a Windows Live ID login button. However, there are cases where the application will want the user to be redirected to the login page from code. To cover this scenario, you need to add a small method called Login to your controller:

        public ActionResult Login(string returnUrl)
        {
            var redirect = HttpContext.Request.Browser.IsMobileDevice ?
            this.windowsLogin.GetMobileLoginUrl(returnUrl) :
            this.windowsLogin.GetLoginUrl(returnUrl);
            return Redirect(redirect);
        }

This method simply retrieves the login URL for Windows Live and redirects the user to that location. This also satisfies a configuration value in your web.config file for ASP.NET Forms Authentication in that any request requiring authentication will be redirected to this method:

<authentication mode="Forms">
      <forms loginUrl="~/Account/Login" name="XAUTH" timeout="2880" path="~/" />
</authentication>

Retrieving the Profile for the Current User

Now with the authentication methods defined, which satisfies your first goal for this controller-signing users in and out in the application-you can move on to retrieving data for the current user.

The Index method, which is the default method for the controller based on the URL mapping configuration in Global.asax, will be where you retrieve the current user's data and return a view displaying that data. The Index method that was initially created when the AccountController class was created should be replaced with the following:

        [Authorize()]
        [AcceptVerbs(HttpVerbs.Get)]
        public ActionResult Index(string returnUrl)
        {
            var profile = this.membershipService.GetCurrentProfile();
            var model = new ProfileViewModel
            {
                Profile = profile,
                ReturnUrl = returnUrl ?? this.GetReturnUrl()
            };
            this.InjectStatesAndActivityTypes(model);
            return View("Index", model);
        }

Visual Studio 2005 Attributes, such as [Authorize()], might not have been in common use in Visual Studio 2005; however, ASP.NET MVC makes use of them often. Attributes allow for metadata to be defined about the target they decorate. This allows for the information to be examined at run time (via reflection) and for action to be taken if deemed necessary.

The Authorize attribute is very handy because it declares that this method can be invoked only for http requests that are already authenticated. If a request is not authenticated, it will be redirected to the ASP.NET Forms Authentication configured login target, which you just finished setting up. The AcceptVerbs attribute also restricts how this method can be invoked, by specifying which Http verbs can be used. In this case, you are restricting this method to HTTP GET verb requests. You've added a string parameter, returnUrl, to the method signature so that when the user is finished viewing or updating her information, she can be returned to what she was looking at previously.

Note This highlights a part of the ASP.NET MVC framework called Model Binding, details of which are beyond the scope of this book. However, you should know that it attempts to find a source for returnUrl (a form field, routing table data, or query string parameter with the same name) and binds it to this value when invoking the method. If the Model Binder cannot find a suitable source, the value will be null. This behavior can cause problems for value types that cannot be null, because it will throw an InvalidOperationException.

The main portion of this method is straightforward: it takes the return of the GetCurrentProfile method on the ASP.NET Membership Service interface and sets up a view model object for the view to use. The call to GetReturnUrl is an example of an extension method defined in the PlanMyNight.Infrastructure project. It's not a member of the Controller class, but in the development environment it makes for much more readable code.(See Figure 6-5.)

Figure-6-5.gif

FIGURE 6-5 Example of extension methods in MvcExtensions.cs

Visual Studio 2005 In .NET Framework 2.0, which Visual Studio 2005 used, extension methods did not exist. Rather than calling this.GetReturnUrl() and also having the method appear in IntelliSense for this object, you would have to type MvcExtensions.GetReturnUrl(this), passing in the controller as a parameter. Extension methods certainly make the code more readable and do not require the developer to know the static class the extension method exists under. For IntelliSense to work, the namespace needs to be listed in the using clauses.

InjectStatesAndActivityTypes is a method you need to implement. It gathers data from the reference repository for names of states and the activity repository. It makes two collections of SelectListItem (an HTML class for MVC): one for the list of states, and the other for the list of different activity types available in the application. It also sets the respective value.

        private void InjectStatesAndActivityTypes(ProfileViewModel model)
        {
            var profile = model.Profile;
            var types = this.activitiesRepository.RetrieveActivityTypes().Select(
            o => new SelectListItem
            {
                Text = o.Name,
                Value = o.Id.ToString(),
                Selected = (profile != null && o.Id ==
                profile.PreferredActivityTypeId)
            }).ToList();
            types.Insert(0, new SelectListItem { Text = "Select...", Value = "0" });
            var states = this.referenceRepository.RetrieveStates().Select(
            o => new SelectListItem
            {
                Text = o.Name,
                Value = o.Abbreviation,
                Selected = (profile != null && o.Abbreviation ==
                profile.State)
            }).ToList();
            states.Insert(0, new SelectListItem
            {
                Text = "Any state",
                Value = string.Empty
            });
            model.PreferredActivityTypes = types;
            model.States = states;
        }

Visual Studio 2005 In Visual Studio 2005, the InjectStatesAndActivities method takes longer to implement because a developer cannot use the LINQ extensions (the call to Select) and Lambda expressions, which are a form of anonymous delegate that the Select method applies to each member of the collection being enumerated. Instead, the developer would have to write out his own loop and enumerate each item manually.

Updating the Profile Data

Having completed the infrastructure needed to retrieve data for the current profile, you can move on to updating the data in the model from a form submission by the user. After this, you can create your view pages and see how all this ties together. The Update method is simple; however, it does introduce some new features not seen yet:

        [Authorize()]
        [AcceptVerbs(HttpVerbs.Post)]
        [ValidateAntiForgeryToken()]
        public ActionResult Update(UserProfile profile)
        {
            var returnUrl = Request.Form["returnUrl"];
            if (!ModelState.IsValid)
            {
                // validation error
                return this.IsAjaxCall() ? new JsonResult
                {
                    JsonRequestBehavior =
                        JsonRequestBehavior.AllowGet,
                    Data = ModelState
                }
                : this.Index(returnUrl);
            }
            this.membershipService.UpdateProfile(profile);
            if (this.IsAjaxCall())
            {
                return new JsonResult
                {
                    JsonRequestBehavior = JsonRequestBehavior.AllowGet,
                    Data = new { Update = true, Profile = profile, ReturnUrl = returnUrl }
                };
            }
            else
            {
                return RedirectToAction("UpdateSuccess", "Account", new
                {
                    returnUrl =
                        returnUrl
                });
            }
        }

The ValidateAntiForgeryToken attribute ensures that the form has not been tampered with. To use this feature, you need to add an AntiForgeryToken to your view's input form. The check on the ModelState to see whether it is valid is your first look at input validation. This is a look at the server-side validation, and ASP.NET MVC offers an easy-to-use feature to make sure that incoming data meets some rules. The UserProfile object that is created for input to this method, via MVC Model Binding, has had one of its properties decorated with a
System.ComponentModel.DataAnnotations.Required attribute. During Model Binding, the MVC framework evaluates DataAnnotation attributes and marks the ModelState as valid only when all of the rules pass.

In the case where the ModelState is not valid, the user is redirected to the Index method where the ModelState will be used in the display of the input form. Or, if the request was an AJAX call, a JsonResult is returned with the ModelState data attached to it.

Visual Studio 2005 Because in ASP.NET MVC requests are routed through controllers rather than pages, the same URL can handle a number of requests and respond with the appropriate view. In Visual Studio 2005, a developer would have to create two different URLs and call a method in a third class to perform the functionality.

When the ModelState is valid, the profile is updated in the membership service and a JSON result is returned for AJAX requests with the success data, or in the case of "normal" requests, the user is redirected to the UpdateSuccess action on the Account controller. The UpdateSuccess method is the final method you need to implement to finish off this controller:

        public ActionResult UpdateSuccess(string returnUrl)
        {
            var model = new ProfileViewModel
            {
                Profile = this.membershipService.GetCurrentProfile(),
                ReturnUrl = returnUrl
            };
            return View(model);
        }

The method is used to return a success view to the browser, display some of the updated data, and provide a link to return the user to where she was when she started the profile update process.

Now that you've reached the end of the Account controller implementation, you should have a class that resembles the following listing:

using System;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Microsoft.Samples.PlanMyNight.Data;
using Microsoft.Samples.PlanMyNight.Entities;
using Microsoft.Samples.PlanMyNight.Infrastructure;
using Microsoft.Samples.PlanMyNight.Infrastructure.Mvc;
using Microsoft.Samples.PlanMyNight.Web.ViewModels;
using WindowsLiveId;
namespace Microsoft.Samples.PlanMyNight.Web.Controllers
{
    [HandleErrorWithContentType()]
    [OutputCache(NoStore = true, Duration = 0, VaryByParam = "*")]
    public class AccountController : Controller
    {
        private readonly IWindowsLiveLogin windowsLogin;
        private readonly IMembershipService membershipService;
        private readonly IFormsAuthentication formsAuthentication;
        private readonly IReferenceRepository referenceRepository;
        private readonly IActivitiesRepository activitiesRepository;
        public AccountController() :
            this(new ServiceFactory().GetMembershipService(), new WindowsLiveLogin(true),
        new FormsAuthenticationService(),
        new ServiceFactory().GetReferenceRepositoryInstance(),
        new ServiceFactory().GetActivitiesRepositoryInstance())
        {
        }
        public AccountController(IMembershipService membershipService,
        IWindowsLiveLogin windowsLogin,
        IFormsAuthentication formsAuthentication,
        IReferenceRepository referenceRepository,
        IActivitiesRepository activitiesRepository)
        {
            this.membershipService = membershipService;
            this.windowsLogin = windowsLogin;
            this.formsAuthentication = formsAuthentication;
            this.referenceRepository = referenceRepository;
            this.activitiesRepository = activitiesRepository;
        }
        public ActionResult LiveId()
        {
            string action = Request.QueryString["action"];
            switch (action)
            {
                case "logout":
                    this.formsAuthentication.SignOut();
                    return Redirect("~/");
                case "clearcookie":
                    this.formsAuthentication.SignOut();
                    string type;
                    byte[] content;
                    this.windowsLogin.GetClearCookieResponse(out type, out content);
                    return new FileStreamResult(new MemoryStream(content), type);
                default:
                    // login
                    NameValueCollection tokenContext;
                    if ((Request.HttpMethod ?? "GET").ToUpperInvariant() == "POST")
                    {
                        tokenContext = Request.Form;
                    }
                    else
                    {
                        tokenContext = new NameValueCollection(Request.QueryString);
                        tokenContext["stoken"] =
                        System.Web.HttpUtility.UrlEncode(tokenContext["stoken"]);
                    }
                    var liveIdUser = this.windowsLogin.ProcessLogin(tokenContext);
                    if (liveIdUser != null)
                    {
                        var returnUrl = liveIdUser.Context;
                        var userId = new Guid(liveIdUser.Id).ToString();
 
                        if (!this.membershipService.ValidateUser(userId, userId))
                        {
                            this.formsAuthentication.SignIn(userId, false);
                            this.membershipService.CreateUser(
                            userId, userId, string.Empty);
                            var profile =
                            this.membershipService.CreateProfile(userId);
                            profile.FullName = "New User";
                            profile.State = string.Empty;
                            profile.City = string.Empty;
                            profile.PreferredActivityTypeId = 0;
                            this.membershipService.UpdateProfile(profile);
                            if (string.IsNullOrEmpty(returnUrl)) returnUrl = null;
                            return RedirectToAction("Index", new
                            {
                                returnUrl =
                                    returnUrl
                            });
                        }
                        else
                        {
                            this.formsAuthentication.SignIn(userId, false);
                            if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/";
                            return Redirect(returnUrl);
                        }
                    }
                    break;
            }
            return Redirect("~/");
        }
        public ActionResult Login(string returnUrl)
        {
            var redirect = HttpContext.Request.Browser.IsMobileDevice ?
            this.windowsLogin.GetMobileLoginUrl(returnUrl) :
            this.windowsLogin.GetLoginUrl(returnUrl);
            return Redirect(redirect);
        }
        [Authorize()]
        [AcceptVerbs(HttpVerbs.Get)]
        public ActionResult Index(string returnUrl)
        {
            var profile = this.membershipService.GetCurrentProfile();
            var model = new ProfileViewModel
            {
                Profile = profile,
                ReturnUrl = returnUrl ?? this.GetReturnUrl()
            };
            this.InjectStatesAndActivityTypes(model);
            return View("Index", model);
        }
        [Authorize()]
        [AcceptVerbs(HttpVerbs.Post)]
        [ValidateAntiForgeryToken()]
 
        public ActionResult Update(UserProfile profile)
        {
            var returnUrl = Request.Form["returnUrl"];
            if (!ModelState.IsValid)
            {
                // validation error
                return this.IsAjaxCall() ?
                new JsonResult
                {
                    JsonRequestBehavior =
                        JsonRequestBehavior.AllowGet,
                    Data = ModelState
                }
                : this.Index(returnUrl);
            }
            this.membershipService.UpdateProfile(profile);
            if (this.IsAjaxCall())
            {
                return new JsonResult
                {
                    JsonRequestBehavior = JsonRequestBehavior.AllowGet,
                    Data = new
                    {
                        Update = true,
                        Profile = profile,
                        ReturnUrl = returnUrl
                    }
                };
            }
            else
            {
                return RedirectToAction("UpdateSuccess",
                "Account", new { returnUrl = returnUrl });
            }
        }
        public ActionResult UpdateSuccess(string returnUrl)
        {
            var model = new ProfileViewModel
            {
                Profile = this.membershipService.GetCurrentProfile(),
                ReturnUrl = returnUrl
            };
            return View(model);
        }
        private void InjectStatesAndActivityTypes(ProfileViewModel model)
        {
            var profile = model.Profile;
            var types = this.activitiesRepository.RetrieveActivityTypes()
            .Select(o => new SelectListItem
            {
                Text = o.Name,
                Value = o.Id.ToString(),
                Selected = (profile != null &&
                o.Id == profile.PreferredActivityTypeId)
            })
            .ToList();
            types.Insert(0, new SelectListItem { Text = "Select...", Value = "0" });
            var states = this.referenceRepository.RetrieveStates().Select(
            o => new SelectListItem
            {
                Text = o.Name,
                Value = o.Abbreviation,
                Selected = (profile != null &&
                o.Abbreviation == profile.State)
            })
            .ToList();
            states.Insert(0,
            new SelectListItem
            {
                Text = "Any state",
                Value = string.Empty
            });
            model.PreferredActivityTypes = types;
            model.States = states;
        }
    }
}

Creating the Account View

In the previous section, you created a controller with functionality that allows a user to update her information and view it. In this section, you're going to walk through the Visual Studio 2010 features that enable you to create the views that display this functionality to the user.

To create the Index view for the Account controller:

  1. Navigate to the Views folder in the PlanMyNight.Web project.

  2. Click the right mouse button on the Views folder, expand the Add submenu, and select New Folder.

  3. Name the new folder Account.

  4. Click the right mouse button on the new Account folder, expand the Add submenu, and select View.

  5. Fill out the Add View dialog box as shown here:

    Figure-6-5.1.gif
     

  6. Click OK. You should see an HTML page with some <asp:Content> controls in the markup:

    Figure-6-5.2.gif

    You might notice that it doesn't look much different from what you are used to seeing in Visual Studio 2005. By default, ASP.NET MVC 2 uses the ASP.NET Web Forms view engine, so there will be some commonality between MVC and Web Forms pages. The primary differences at this point are that the page class derives from System.Web.Mvc.ViewPage<ProfileViewModel> and there is no code-behind file. MVC does not use code-behind files, like ASP.NET Web Forms does, to enforce a strict separation of concerns. MVC pages are generally edited in markup view; the designer view is primarily for ASP.NET Web Forms applications.

For this page skeleton to become the main view for the Account controller, you should change the title content to be more in line with the other views:

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
      Plan My Night - Profile
</asp:Content>

Next you need to add the client scripts you are going to use in the content placeholder for the HtmlHeadContent:

<asp:Content ID="Content3" ContentPlaceHolderID="HtmlHeadContent" runat="server">
<% Ajax.RegisterClientScriptInclude(
Url.Content("~/Content/Scripts/jquery-1.3.2.min.js"),
"http://ajax.Microsoft.com/ajax/jQuery/jquery-1.3.2.min.js"); %>
<% Ajax.RegisterClientScriptInclude(
Url.Content("~/Content/Scripts/jquery.validate.js"),
"http://ajax.microsoft.com/ajax/jquery.validate/1.5.5/jquery.validate.min.js"); %>
<% Ajax.RegisterCombinedScriptInclude(
Url.Content("~/Content/Scripts/MicrosoftMvcJQueryValidation.js"), "pmn"); %>
<% Ajax.RegisterCombinedScriptInclude(
Url.Content("~/Content/Scripts/ajax.common.js"), "pmn"); %>
<% Ajax.RegisterCombinedScriptInclude(
Url.Content("~/Content/Scripts/ajax.profile.js"), "pmn"); %>
<%= Ajax.RenderClientScripts() %>
</asp:Content>

This script makes use of extension methods for the System.Web.Mvc.AjaxHelper, which are found in the PlanMyNight.Infrastructure project, under the MVC folder.

With the head content set up, you can look at the main content of the view:

<asp:Content ContentPlaceHolderID="MainContent" runat="server">
      <div class="panel" id="profileForm">
            <div class="innerPanel">
                  <h2>
                        <
span>My Profile</span>
                  </h2>
                  <
% Html.EnableClientValidation(); %>
<% using (Html.BeginForm("Update", "Account")) %>
<% { %>
<%=Html.AntiForgeryToken()%>
                  <div class="items">
                        <fieldset>
                              <
p>
                                    <
label for="FullName">Name:</label>
                                    <%=Html.EditorFor(m => m.Profile.FullName)%>
                                    <%=Html.ValidationMessage("Profile.FullName",
                                    new { @class = "field-validation-error-wrapper" })%>
                              </p>
                              <
p>
                                    <
label for="State">State:</label>
                                    <%=Html.DropDownListFor(m => m.Profile.State, Model.States)%>
                              </p>
                              <
p>
                                    <
label for="City">City:</label>
                                    <%=Html.EditorFor(m => m.Profile.City, Model.Profile.City)%>
                              </p>
                              <
p>
                                    <
label for="PreferredActivityTypeId">Preferred activity:</label>
                                    <%=Html.DropDownListFor(m =>
                                    m.Profile.PreferredActivityTypeId,
                                    Model.PreferredActivityTypes)%>
                              </p>
                        </
fieldset>
                        <
div class="submit">
                              <%=Html.Hidden("returnUrl", Model.ReturnUrl)%>
                              <%=Html.SubmitButton("submit", "Update")%>
                        </div>
                  </
div>
                  <
div class="toolbox"></div>
                  <% } %>
            </div>
      </
div>
</asp:Content>

Aside from some inline code, this looks to be fairly normal HTML markup. We're going to focus our attention on the inline code pieces to demonstrate the power they bring (as well as the simplicity).

Visual Studio 2005 In Visual Studio 2005, it was more commonplace to use server-side controls to display data, and other display-time logic. However, because ASP.NET MVC view pages do not have a code-behind file, server-side logic executed in the view at render time must be done in the same file with the markup. ASP.NET Web Forms controls can still be used. Our example makes use of the <asp:Content> control. However, the functionality of ASP.NET Web Forms controls is generally limited because there is no code-behind file.

MVC makes a lot of use of what is known as HTML helpers. The methods contained under System.Web.Mvc.HtmlHelper emit small, standards-compliant HTML tags for various uses. This requires the MVC developer to type more markup than a Web Forms developer in some cases, but the developer has more direct control over the output. The strongly typed version of this extension class (HtmlHelper<TModel>) can be referenced in the view markup via the ViewPage<TModel>.Html property.

These are the HTML methods used in this form, which are only a fraction of what is available by default:

  • Html.EnableClientValidation enables data validation to be performed on the client side based on the strongly typed ModelState dictionary.

  • Html.BeginForm places a <form> tag in the markup and closes the form at the end of the using section. It takes various parameters for options, but the most common parameter is the name of the action and the controller to invoke that action on. This allows the MVC framework to generate the specific URL to target the form to at run time, rather than having to input a string URL into the markup.

  • Html.AntiForgeryToken places a hidden field in the form with a check value that is also stored in a cookie in the visitor's browser and validated when the target of the form has the ValidateAntiForgeryToken attribute. Remember that you added this attribute to the Update method in the controller.

  • Html.EditorFor is an overloaded method that inserts a text box into the markup. This is the strongly typed version of the Html.Editor method.

  • Html.DropDownListFor is an overloaded method that places a drop-down list into the markup. This is the strongly typed version of the Html.DropDownList method.

  • Html.ValidationMessage is a helper that will display a validation error message when a given key is present in the ModelState dictionary.

  • Html.Hidden places a hidden field in the form, with the name and value that is passed in.

  • Html.SubmitButton creates a Submit button for the form.

Note With the Index view markup complete, you only need to add the view for the UpdateSuccess action before you can see your results.

To create the UpdateSuccess view:

  1. Expand the PlanMyNight.Web project in Solution Explorer, and then expand the Views folder.

  2. Click the right mouse button on the Account folder.

  3. Open the Add submenu, and click View.

  4. Fill out the Add View dialog box so that it looks like this:

    Figure-6-5.3.gif

    After the view page is created, fill in the title content so that it looks like this:

    <asp:Content ContentPlaceHolderID="TitleContent" runat="server">Plan My Night - Profile Updated</asp:Content>

    And the placeholder for MainContent should look like this:

    <
    asp:Content ContentPlaceHolderID="MainContent" runat="server">
          <div class="panel" id="profileForm">
                <div class="innerPanel">
                      <h2>
                            <
    span>My Profile</span>
                      </h2>
                      <
    div class="items">
                            <p>Your profile has been successfully updated.</p>
                            <h3>
                                  » <a href=""
                                        <%=Html.AttributeEncode(Model.ReturnUrl ??
                                        Url.Content("~/"))%>">Continue
                                  </a>
                            </
    h3>
                      </
    div>
                      <
    div class="toolbox"></div>
                </div>
          </
    div>
    </asp:Content>

To see the views created, you must perform an edit to the Site.Master file (located in the Views/Shared folder from the Web project's root). Line 33 of the file is commented out, and the comment tags should be removed so that it matches the following example:

<%=Html.ActionLink<AccountController>(c =>c.Index(null), "My Profile")%>

With this last view created, you can now compile and launch the application. Click the Sign In button, as seen in the top right corner of Figure 6-6, and sign in to Windows Live ID.

Figure-6-6.gif

FIGURE 6-6 Plan My Night default screen

After you've signed in, you should be redirected to the Index view of the Account controller you created, shown in Figure 6-7.

Figure-6-7.gif

FIGURE 6-7 Profile settings screen returned from the Index method of the Account controller

If instead you are returned to the search page, just click the My Profile link, located in the links at the center and top of the interface. To see the new data-validation features at work, try to save the form without filling in the Full Name field. You should get a result that looks like Figure 6-8.

Figure-6-8.gif

FIGURE 6-8 Example of failed validation during Model Binding checks

Because you enabled client-side validation, there was no post back. To see the server-side validation work, you would have to edit the Index.aspx file in the Account folder and comment out the call to Html.EnableClientValidation. The tight integration and support of AJAX and other JavaScript in MVC applications allows for server-side operations such as validation to be moved to the client side much more easily than they were previously.
Visual Studio 2005 In ASP.NET MVC applications, the value of the ID attribute for a particular HTML element is not transformed, like it is in ASP.NET Web Forms 2.0. In Visual Studio 2005, a developer would have to make sure to set the UniqueID of a control/element into a JavaScript variable so that it could be accessed by external JavaScript. This was done to make sure the ID was unique. However, it was always an extra layer of complexity added to the interaction between ASP.NET 2.0 Web Forms controls and JavaScript. In MVC, this transformation does not happen, but it is up to the developers to ensure uniqueness of the ID. It should also be noted that ASP.NET 4.0 Web Forms now supports disabling the ID transformation on a per-control basis, if the developer so wishes.

With the completed Account controller and related views, you have filled in the missing "core" functionality of Plan My Night, while taking a brief tour of some new features in Visual Studio 2010 and MVC 2.0 applications. But MVC is not the only choice for Web developers. ASP.NET Web Forms has been the primary application type for ASP.NET since it was released, and it continues to be improved upon in Visual Studio 2010. In the next section, we'll explore creating an ASP.NET Web Form with the Visual Designer to be used in the MVC application.

Using the Designer View to Create a Web Form

Applications will encounter an unexpected condition at some point in their lifetime of use. The companion application is no different, and when it does encounter an unexpected condition, it returns an error screen like that shown in Figure 6-9.

Figure-6-9.gif

FIGURE 6-9 Example of an error screen in the Plan My Night application

Currently, a user who sees this screen really has only the option of trying his action again or using the navigation links along the top area of the application. (Of course, that might also cause another error.) Adding an option for the user to provide feedback allows the developers to gain information about the situation that might not be apparent by using the standard exception message and stack trace. To show a different way to create a user interface component for Plan My Night, the error feedback page is going to be created as an ASP.NET Web Form using primarily the Designer view in Visual Studio. Before you can begin designing the form, you need to create a base form file to work from.

To create a new Web form:

  1. Open the context menu on the PlanMyNight.Web project (by clicking the right mouse button), open the Add submenu, and select New Item.

  2. In the Add New Item dialog box, select Web Form Using Master Page and call the item ErrorFeedback.aspx in the Name field.

    Figure-6-9.1.gif
     

  3. The dialog screen to associate a master page with this Web form will appear. On the Project Folders side, ensure that the main PlanMyNight.Web folder is selected and then select the WebForms.Master item on the right.

    Figure-6-9.2.gif
     

  4. The resulting page can be shown in the source mode (or Design view) instead of Split view. Switch the view to Split (located at the bottom of the window, just like in previous Visual Studio versions). When you are done, the screen should look similar to this:

    Figure-6-9.3.gif

    Note Split view is recommended so that you can see the source the designer is generating and to add extra markup as needed.

It's a good idea to pin the control toolbox open on the screen because you'll be dragging controls and elements to the content area during this section. The toolbox, if not present already, can be found under the View menu.

Start by dragging a div element (under the HTML group) from the toolbox into the MainContent section of the designer. A div tab will appear, indicating that the new element you added is the currently selected element. Open the context menu for the div, and choose Properties (which can also be opened by pressing the F4 key). With the Properties window open, edit the (Id) property to have a value of profileForm. (Casing is important.) Also, change the Class property to have a value of panel. After editing the values, the size of your content area will have changed, because CSS is applied in the Design view.

Visual Studio 2005 A much-needed update to the Web Forms designer surface from Visual Studio 2005 is the application of CSS. This allows the developer to see in real-time how the style changes are applied, without having to run the application. When viewed in Visual Studio 2005, the designer for the search.aspx page will appear similar to Figure 6-10.

Figure-6-10.gif

FIGURE 6-10 Designer view of an ASP.NET Web page in Visual Studio 2005

Drag another div inside the first one, and set its class property to innerPanel. In the markup panel, add the following markup to the innerPanel:

<h2>
      <
span>Error Feedback</span>
</h2>

After the close of the <h2> tag, add a new line and open the context menu. Choose Insert Snippet, and follow the click path of ASP.NET > formr. This will create a server-side form tag for you to insert Web controls into. Inside the form tag, place a div tag with the class attribute set to items and then a fieldset tag inside the div tag.

Next drag a TextBox control (found under Standard) from the toolbox and drop it inside the fieldset tag. Set the ID of the text box to FullName. Add a <label> tag before this control in the markup view, set its for property to the ID of the text box, and set its value to Full Name: (making sure to include the colon). To set the value of a <label> tag, place the text between the <label> and </label> tags. Surround these two elements with a <p>, and you should have something like Figure 6-11 in the Design view.

Figure-6-11.gif

FIGURE 6-11 Current state of ErrorFeedback.aspx in the Design view

Add another text box and label it in a similar manner as the first, but set the ID of the text box to EmailAddress and the label value to Email Address: (making sure to include the colon). Repeat the process a third time, setting the TextBox ID and label value to Comments. There should now be three labels and three single-line TextBox controls in the Design view. The Comments control needs multiline input, so open its property page and set TextMode to Multiline, Rows to 5, and Columns to 40. This should create a much wider text box in which the user can enter comments.

Use the Insert Snippet feature again, after the Comments text box, and insert a "div with class" tag (HTML>divc). Set the class of the div tag to submit, and drag a Button control from the toolbox into this div. Set the Button's Text property to Send Feedback.

The designer should show something similar to what you see in Figure 6-12, and at this point you have a page that will submit a form.

Figure-6-12.gif

FIGURE 6-12 The ErrorFeedback.aspx form with a complete field set

However, it does not perform any validation on the data being submitted. To do this, you'll take advantage of some of the validation controls present in ASP.NET. You'll make the Full Name and Comments boxes required fields and perform a regex validation of the e-mail address to ensure that it matches the right pattern.

Under the Validation group of the toolbox are some premade validation controls you'll use. Drag a RequiredFieldValidator object from the toolbox, and drop it to the right of the Full Name text box. Open the properties for the validation control, and set the ControlToValidate property to FullName. (It's a drop-down list of controls on the page.) Also, set the CssClass to field-validation-error. This changes the display of the error to a red triangle used elsewhere in the application. Finally, change the Error Message property to Name is Required. (See Figure 6-13.)

Figure-6-13.gif

FIGURE 6-13 Validation control example

Repeat these steps for the Comments box, but substitute the ErrorMessage and ControlToValidate property values as appropriate.
For the Email Address field, you want to make sure the user types in a valid e-mail address, so for this field drag a RegularExpressionValidator control from the toolbox and drop it next to the Email Address text box. The property values are similar for this control in that you set the ControlToValidate property to EmailAddress and the CssClass property to field-validation-error. However, with this control you define the regular expression to be applied to the input data. This is done with the ValidationExpression property, and it should be set like this:

[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}

The error message for this validator should say something like "Must enter a valid e-mail address."

The form is complete. To see it in the application, you need to add the option of providing feedback to a user when the user encounters an error. In Solution Explorer, navigate the PlanMyNight.Web project tree to the Views folder and then to the Shared subfolder. Open the Error.aspx file in the markup viewer, and go to line 35. This is the line of the error screen where you ask the user if she wants to try her action again and where you'll put the option for sending the feedback. After the question text in the same paragraph, add the following markup:

or
<a href="/ErrorFeedback.aspx">send feedback</a>?

This will add an option to go to the form you just created whenever there is a general error in the MVC application. To see your form, you'll have to cause an error in your application.

To cause an error in the Plan My Night application:

  1. Start the application.

  2. After the default search page is up, type the following into the browser address bar:
    http://www.planmynight.net:48580/Itineraries/Details/38923828.

  3. Because it is highly unlikely such an itinerary ID exists in the database, an error screen will be shown.

    Figure-6-13.1.gif
     

  4. With the error screen visible, click the link to go to the feedback form. Try to submit the form with invalid data.

    Figure-6-13.2.gif

    ASP.NET uses client-side script (when the browser supports it) to perform the validation, so no postbacks occur until the data passes. On the server side, when the server does receive a postback, a developer can check the validation state with the Page.IsValid property in the code-behind. However, because you used client-side validation (which is on by default), this will always be true. The only code in the code-behind that needs to be added is to redirect the user on a postback (and check the Page.IsValid property, in case client validation missed something):

            protected void Page_Load(object sender, EventArgs e)
            {
                if (this.IsPostBack && this.IsValid)
                {
                    this.Response.Redirect("/", true);
                }
            }

This really isn't very useful to the user, but our goal in this section was to work with the designer to create an ASP.NET Web Form. This added a new interface to the PlanMyNight .Web project, but what if you wanted to add new functionality to the application in a more modular sense, such as some degree of functionality that can be added or removed without having to compile the main application project. This is where an extensibility framework like the Managed Extensibility Framework (MEF) can show the benefits it brings.

Extending the Application with MEF

A new technology available in Visual Studio 2010 as part of the .NET Framework 4 is the Managed Extensibility Framework (MEF). The Managed Extensibility Framework provides developers with a simple (yet powerful) mechanism to allow their applications to be extended by third parties after the application has been shipped. Even within the same application, MEF allows developers to create applications that completely isolate components, allowing them to be managed or changed independently. It uses a resolution container to map components that provide a particular function (exporters) and components that require that functionality (importers), without the two concrete components having to know about each other directly. Resolutions are done on a contract basis only, which easily allows components to be interchanged or introduced to an application with very little overhead.

See Also MEF's community Web site, containing in-depth details about the architecture, can be found at http://mef.codeplex.com.

The companion Plan My Night application has been designed with extendibility in mind, and it has three "add-in" module projects in the solution, under the Addins solution folder. (See Figure 6-14.)

Figure-6-14.gif

FIGURE 6-14 The Plan My Night application add-ins

PlanMyNight.Addins.EmailItinerary adds the ability to e-mail itinerary lists to anyone the user sees fit to receive them.

PlanMyNight.Addins.PrintItinerary provides a printer-friendly view of the itinerary. Lastly, PlanMyNight.Addins.Share adds in social-media sharing functions (so that the user can post a link to an itinerary) as well as URL-shortening operations. None of these projects reference the main PlanMyNight.Web application or are referenced by it. They do have references to the PlanMyNight.Contracts and PlanMyNight.Infrastructure projects, so they can export (and import in some cases) the correct contracts via MEF as well as use any of the custom extensions in the infrastructure project.

Note Before doing the next step, if the Web application is not already running, launch the PlanMyNight.Web project so that the UI is visible to you.
To add the modules to your running application, run the DeployAllAddins.bat file, found in the same folder as the PlanMyNight.sln file. This will create new folders under the Areas section of the PlanMyNight.Web project. These new folders, one for each plug-in, will contain the files needed to add their functionality to the main Web application. The plug-ins appear in the application as extra options under the current itinerary section of the search results page and on the itinerary details page. After the batch file is finished running, go to the interface for PlanMyNight, search for an activity, and add it to the current itinerary. You should notice some extra options under the itinerary panel other than just New and Save. (See Figure 6-15.)

Figure-6-15.gif

FIGURE 6-15 Location of the e-mail add-in in the UI

The social sharing options will show in the interface only after the itinerary is saved and marked public. (See Figure 6-16.)

Figure-6-16.gif

FIGURE 6-16 Location of the social-sharing add-in in the UI

Visual Studio 2005 Visual Studio 2005 does not have anything that compares to MEF. To support plug-ins, a developer would have to either write the plug-in framework from scratch or purchase a commercial package. Either of the two options led to proprietary solutions an external developer would have to understand in order to create a component for them. Adding MEF to the .NET Framework helps to cut down the entry barriers to producing extendible applications and the plug-in modules for them.

Print Itinerary Add-in Explained

To demonstrate how these plug-ins wire into the application, let's have a look at the PrintItinerary.Addin project. When you expand the project, you should see something like the structure shown in Figure 6-17.

Figure-6-17.gif

FIGURE 6-17 Structure of the PrintItinerary project

Some of this structure is similar to the PlanMyNight.Web project (Controllers and Views).That's because this add-in will be placed in an MVC application as an area. If you look more closely at the PrintItineraryController.cs file in the Controller folder, you can see it is similar in structure to the controller you created earlier in this chapter (and similar to any of the other controllers in the Web application). However, some key differences set it apart from the controllers that are compiled in the primary PlanMyNight.Web application.

Focusing on the class definition, you'll notice some extra attributes:

[Export("PrintItinerary", typeof(IController))]
[PartCreationPolicy(CreationPolicy.NonShared)]

These two attributes describe this type to the MEF resolution container. The first attribute, Export, marks this class as providing an IController under the contract name of PrintItinerary. The second attribute declares that this object supports only nonshared creation and cannot be created as a shared/singleton object. Defining these two attributes are all you need to do to have the type used by MEF. In fact, PartCreationPolicy is an optional attribute, but it should be defined if the type cannot handle all the creation policy types.

Further into the PrintItineraryController.cs file, the constructor is decorated with an ImportingConstructor attribute:

[ImportingConstructor]
public PrintItineraryController(IServiceFactory serviceFactory) :
this(
serviceFactory.GetItineraryContainerInstance(),
serviceFactory.GetItinerariesRepositoryInstance(),
serviceFactory.GetActivitiesRepositoryInstance())
{
}

The ImportingConstructor attribute informs MEF to provide the parameters when creating this object. In this particular case, MEF provides an instance of IServiceFactory for this object to use. Where the instance comes from is of no concern to the this class and really assists with creating modular applications. For our purposes, the IServiceFactory contracted is being exported by the ServiceFactory.cs file in the PlanMyNight.Web project.

The RouteTableConfiguration.cs file registers the URL route information that should be directed to the PrintItineraryController. This route, and the routes of the other add-ins, are registered in the application during the Application_Start method in the Global.asax.cs file of PlanMyNight.Web:

            // MEF Controller factory
            var controllerFactory = new MefControllerFactory(container);
            ControllerBuilder.Current.SetControllerFactory(controllerFactory);
            // Register routes from Addins
            foreach (RouteCollection routes in container.GetExportedValues<RouteCollection>())
            {
                foreach (var route in routes)
                {
                    RouteTable.Routes.Add(route);
                }
            }

The controllerFactory, which was initialized with an MEF container containing path information to the Areas subfolder (so that it enumerated all the plug-ins), is assigned to be the controller factory for the lifetime of the application. This allows controllers imported via MEF to be usable anywhere in the application. The routes these plug-ins respond to are then retrieved from the MEF container and registered in the MVC routing table.

The ItineraryContextualActionsExport.cs file exports information to create the link to this plug-in, as well as metadata for displaying it. This information is used in the

ViewModelExtensions.cs file, in the PlanMyNight.Web project, when building a view model for display to the user:

            // get addin links and toolboxes
            var addinBoxes = new List<RouteValueDictionary>();
            var addinLinks = new List<ExtensionLink>();
            addinBoxes.AddRange(AddinExtensions.GetActionsFor("ItineraryToolbox", model.Id == 0 ? null : new { id = model.Id }));
            addinLinks.AddRange(AddinExtensions.GetLinksFor("ItineraryLinks", model.Id == 0 ? null : new { id = model.Id }
);


The call to AddinExtensions.GetLinksFor enumerates over exports in the MEF Export provider and returns a collection of them to be added to the local addinLinks collection. These are then used in the view to display more options when they are present.

Summary

In this chapter, we explored a few of the many new features and technologies found in Visual Studio 2010 that were used to create the companion Plan My Night application. We walked through creating a controller and its associated view and how the ASP.NET MVC framework offers Web developers a powerful option for creating Web applications. We also explored how using the Managed Extensibility Framework in application design can allow plug-in modules to be developed external to the application and loaded at run time. In the next chapter, we'll explore how debugging applications has been improved in Visual Studio 2010.

Up Next
    Ebook Download
    View all
    Learn
    View all