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 2008 introduced official support for
AJAX-enabled Web pages, Language Integrated Query (LINQ), plus many other
improvements 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 9\ in a folder called UserInterface-Start.
Introducing the PlanMyNight.Web Project
Note ASP.NET MVC 1.0 Framework is available as an extension to Visual Studio
2008; however, this chapter was written in the context of the user having a
default installation of Visual Studio 2008, which only had support for ASP.NET
Web Forms 3.5 projects.
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 2008. Some items in the project (as seen in Figure 9-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 9-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 9-2.
FIGURE 9-2 A web.config file with build-specific files expanded
Visual Studio 2008 When working on a project in Visual Studio 2008, 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.retail 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
9-3.
FIGURE 9-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 9-4).
FIGURE 9-4 Error screen returned when logging into 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:
- Use Solution Explorer to navigate to the
Controllers folder in the PlanMyNight.Web project, and click the right mouse
button.
- Open the Add submenu and select the
Controller item.
- Fill in the name of the controller as
AccountController.
Note Leave the Add Action Methods For
Create, Update And Details 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 2008 A difference to be noted from developing ASP.NET Web Forms
applications in Visual Studio 2008 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
readonly IWindowsLiveLogin windowsLogin;
private
readonly IMembershipService membershipService;
private
readonly IFormsAuthentication
formsAuthentication;
private
readonly IReferenceRepository
referenceRepository;
private
readonly IActivitiesRepository
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 Defines a functionality
contract for interacting with the Windows Live ID service.
- IMembershipService Defines user profile
information and authorization methods. In your companion application, it is
an abstraction of the ASP.NET Membership Service.
- IFormsAuthentication Defines functionality
for interacting with ASP.NET Forms Authentication abstraction.
- IReferenceRespository Defines reference
resources, such as lists of states and other model-specific information.
- IActivitiesRespository 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())
{
}
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;
}
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 into 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:
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);
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 into
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>
<system.webServer>
<modules
runAllManagedModulesForAllRequests="true"/>
</system.webServer>
<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
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 Controller class, but in the development environment it makes
for much more readable code. (See Figure 9-5.)
FIGURE 9-5 Example of extension methods in MvcExtensions.cs
InjectStatesAndActivityTypes is a method you need to implement in the
AccountController class. 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;
}
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 2008 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 2008, 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
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);
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:
- Navigate to the Views folder in the
PlanMyNight.Web project.
- Click the right mouse button on the Views
folder, expand the Add submenu, and select New Folder.
- Name the new folder Account.
- Click the right mouse button on the new
Account folder, expand the Add submenu, and select View.
- Fill out the Add View dialog box as shown
here:
- Click Add. You should see an HTML page
with some <asp:Content> controls in the markup:
You might notice that it doesn't look much
different from what you are used to seeing in Visual Studio 2008. 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
ID="Content2"
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 2008 In Visual Studio 2008, 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:
- Expand the PlanMyNight.Web project in
Solution Explorer, and then expand the Views folder.
- Click the right mouse button on the
Account folder.
- Open the Add submenu, and click View.
- Fill out the Add View dialog box so that
it looks like this:
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 9-6, and
sign in to Windows Live ID.
FIGURE 9-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 9-7.
FIGURE 9-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 9-8.
FIGURE 9-8 Example of failed validation during Model Binding checks
Because you enabled client-side validation, there was no postback. 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 2008 In ASP.NET MVC applications, the value of the ID attribute
for a particular HTML element is not transformed, like they are in ASP.NET Web
Forms 3.5. In Visual Studio 2008, 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 to the interaction between
ASP.NET 3.5 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 9-9.
FIGURE 9-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:
- Open the context menu on the
PlanMyNight.Web project (by clicking the right mouse button), open the Add
submenu, and select New Item.
- In the Add New Item dialog box, select Web
Form Using Master Page and call the item ErrorFeedback.aspx in the Name
field.
- 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.
- 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:
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.
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 > form. 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 9-10 in the Design view.
FIGURE 9-10 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 9-11, and
at this point you have a page that will submit a form.
FIGURE 9-11 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 combo box
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 9-12.)
FIGURE 9-12 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 whether
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:
- Start the application.
- After the default search page is up, type
the following into the browser address bar:
http://www.planmynight.net:48580/Itineraries/Details/38923828. - Because it is highly unlikely such an
itinerary ID exists in the database, an error screen will be shown. If you
are running the application with the debugger attached, the application will
pause on an exception breakpoint. Continue the application (F5 is the
default key) to see the following screen:
- With the error screen visible, click the
link to go to the feedback form. Try to submit the form with invalid data.
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 9-13.)
FIGURE 9-13 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 Plan My Night, 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 9-14.)
FIGURE 9-14 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 9-15.)
FIGURE 9-15 Location of the social sharing add-in in the UI
Visual Studio 2008 Visual Studio 2008 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 extendable
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 9-16.
FIGURE 9-16 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.