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 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 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 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 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:
- 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 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 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:
-
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 OK. 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 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:
-
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 6-6, and
sign in to Windows Live ID.
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 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 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 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.
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 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 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 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 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:
-
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.
-
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 6-14.)
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 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 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 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.