1. Trang chủ
  2. » Công Nghệ Thông Tin

ASP.NET 4.0 in Practice phần 6 docx

50 355 0

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 50
Dung lượng 15,34 MB

Các công cụ chuyển đổi và chỉnh sửa cho tài liệu này

Nội dung

public class PostMetadata { [DataTypeDataType.MultilineText] public string Text { get; set; } [UIHint"Author"] public int AuthorId { get; set; } } [MetadataTypetypeofPostMetada

Trang 1

public class PostMetadata

{

[DataType(DataType.MultilineText)]

public string Text { get; set; }

[UIHint("Author")]

public int AuthorId { get; set; }

} [MetadataType(typeof(PostMetadata))]

public partial class Post

{ } VB: Public Class PostMetadata <DataType(DataType.MultilineText)>

Public Property Text As String

<UIHint("Author")>

Public Property AuthorId As Integer

End Class <MetadataType(GetType(PostMetadata))>

Partial Public Class Post

End Class

When it comes across properties marked like those at Band C, the ASP.NET MVC view engine looks in the Shared View folder for data templates that have the same names If it finds any such templates, it renders them

DISCUSSION

Modern development platforms provide features that help us build consistent and maintainable UIs Web Forms, for example, uses the abstraction of custom server controls to let the developer build discrete and reusable interface portions ASP.NET MVC provides a different model, called data templates, which is based on the data type you want to represent or edit in your page Anytime you realize there’s a partic-ular object among your models that appears many times in many pages, and you

Listing 9.3 Marking property type with UIHintAttribute

Figure 9.5 Although Title and Text are both strings, we need EditorFor to produce different templates because each has a different business meaning.

Standard data type definition

B

Custom data type definition

C

Metadata type reference

Standard data type definition

B

Custom data type definition

C

Metadata type reference

Trang 2

want to componentize how it gets displayed or how its editor looks when it’s placed

in a form, reach for a custom data template

Think about how many times you’ve built a drop-down list to let the user choose a customer It doesn’t matter whether that user is going to associate it with an order or

an invoice, a property of type Customer is always going to be there to fill; building

a single editor template for it is enough to automatically have it injected wherever it’s needed

Even though they’re powerful, templates almost always require an association to a specific data type, but this isn’t always the rule Consider items like buttons, hyper-links, or pop ups, just to name a few: although they aren’t necessarily bound to a DateTime or Customer object, you might still want to build discrete components and avoid writing the same markup again and again in your pages HTML helpers are much more helpful in these situations, as you’re going to see in the next section

Componentized markup through HTML helpers

Data templates are an extremely smart solution when you must quickly build input forms, or when you need to display complex data On the other hand, sometimes you need to include a bunch of markup code in something that must be as easily reusable

as templates, but not necessarily bound to a particular model type

Let’s think about what happens every time we have to insert a link into a view The link can come from data of different types, involve more than just one property of an object, or even originate from hardcoded values such as the Back To Index link on the post edit page of CoolMVCBlog In all these cases, you’ll find that using an HTML helper called ActionLink is a solution you’ll be satisfied with Besides generating markup, this solution also holds the logic to determine a target URL, given action, and controller names

Similar situations are common in real-world applications, and having a library of customized HTML helpers can surely make the difference for how consistent and maintainable your product will be For that reason, it’s worth trying to learn to build some of your own

PROBLEM

Our application allows registered users to perform login and logout operations using the corresponding actions of SecurityController We want to build a custom compo-nent that we can re-use to easily build a form to insert login credentials or, if the user

is already authenticated, to show a welcome message to the user

SOLUTION

HTML helpers are methods you can call from within a view to generate HTML, sulating all the logic needed to render it Every time we used the ActionLink exten-sion method in chapter 8, we used it not only because we didn’t want to manually write a hyperlink like <a href="someURL" .>Link text</a>, but also because it allowed us to reason in terms of controller and actions, and, fortunately, it also trans-lates it to actual URLs, as in figure 9.6

encap-TECHNIQUE 53

Trang 3

The idea we’ll use to solve our problem is to create a new HTML helper that can ate the request authentication status and generate a login form or welcome message, whichever is appropriate We could easily include the helper in a view, or perhaps in the master page with just this code:

evalu-C# and VB:

<%: Html.Login("Security", "Login", "Logout") %>

Building such an HTML helper is the same as writing a method like the one in the next listing This method accepts actions and a controller name that we want to use when the user is logging in or out

C#:

public static HtmlString Login(this HtmlHelper html,

string controller, string loginAction, string logoutAction)

private static HtmlString WelcomeMessage(HtmlHelper html,

string logoutAction, string controller)

Public Function Login(ByVal html As HtmlHelper,

ByVal controller As String, ByVal loginAction As String,

ByVal logoutAction As String) As HtmlString

HtmlHelper’s extension method

B

Output selection logic

C

Composition

of welcome message

D

HtmlHelper’s extension method

B

Output selection logic

C

Trang 4

End Function

Private Function WelcomeMessage(ByVal html As HtmlHelper,

ByVal logoutAction As String, ByVal controller As String) As HtmlString Return New HtmlString(String.Format("Welcome {0} :: {1}",

Notice how we leverage another HTML helper, ActionLink, to build the hyperlink Then we wrap the whole result using an HtmlString class This class represents a string that contains already encoded HTML, which won’t be affected when it’s dis-played in a <%: %> tag

Conversely, when the user isn’t authenticated, our helper invokes a LoginInput method This method is slightly more complex, because it must use the code shown in the following listing to build an actual HTML form

C#:

private static HtmlString LoginInput(HtmlHelper html,

string loginAction, string controller)

"<input type=\"submit\" value=\"Login\" />");

return new HtmlString(form.ToString());

}

VB:

Private Function LoginInput(ByVal html As HtmlHelper,

ByVal loginAction As String, ByVal controller As String)

D

Composition of Action attribute B

Form’s HTML content

Trang 5

"<input type=""submit"" value=""Login"" />")

Return New HtmlString(form.ToString())

End Function

This code takes advantage of an object called TagBuilder, which eases the task of building HTML tags and decorating them with the attributes we need For an HTML form, for example, we must indicate that we want to post it to a certain destination URL, which we can obtain from the controller and loginInput parameters through ASP.NET MVC’s UrlHelper class B

DISCUSSION

HTML helpers are a simple way to include in a method the logic required to generate HTML code, so that we can easily replicate it when we need it Building them is only a matter of creating an extension method for the HtmlHelper type and returning an HtmlString instance (although the return type can be also a plain string)

Given its extremely versatile nature, these tools give you a great advantage when you’re developing applications in ASP.NET MVC Even though everything you make with

an HTML helper can also be made using partial views, HTML helpers are usually more immediate and easy to use; after all, you just have to invoke a method, and you don’t have to deal with models and types like you do with views and templates Moreover, they’re just code, so you can build class libraries and reuse them across many projects

Of course, there’s always a downside to every great solution You want to be careful not to overuse HTML helpers; they’re usually a bit verbose and tend to replace the actual markup You don’t want to bury the logic that generates the markup because that lessens your control over the HTML—one of the key advantages of using ASP.NET MVC in the first place

In summary, HTML helpers and data templates are two key features of ASP.NET MVC that you can leverage to avoid duplicating the same markup over and over in your views But these techniques cover only half the problem—code duplication often happens in controllers, too The next section will show you a useful trick for avoiding it

Inject logic using action filters

The previous section was about componentizing markup, but sometimes markup requires code on the controller side to render correctly If you were forced to replicate the code each time you wanted to use an HTML helper, a partial view, or a data template, you would lose almost all the advantages of building these reusable components Let’s recall for a moment the homepage we built in chapter 8 It should look like the one in figure 9.7, which highlights a particular portion of it

Composition

of Action attribute

B

Form’s HTML content

TECHNIQUE 54

Trang 6

When we built the corresponding view, we thought the tag cloud would be a shared UI element, which was supposed to be present in multiple pages; this was one of the rea-sons we decided to design it as a partial view Unfortunately, although the template is actually reusable, we still need some code on the controller to populate the model with the data the cloud will represent The HomePageController did it by invoking a TagCloudService, as shown in the following listing If things remain as they are, we’ll have to replicate this code for each action that ultimately shows a tag cloud.

C#:

public ActionResult Index()

{

// more code here

var service = new TagCloudService(ctx);

model.TagCloudItems = service.GetTagCloudItems();

return View(model);

}

VB:

Public Function Index() As ActionResult

' more code here

Dim service = New TagCloudService(ctx)

model.TagCloudItems = service.GetTagCloudItems()

Return View(model)

End Function

It goes without saying that we definitely want to avoid replicating all this We can do it

by using a powerful ASP.NET MVC feature: action filters

Listing 9.6 HomeController fetching tag cloud items

Figure 9.7 Our blog engine’s homepage; it contains a tag cloud that will likely be shared among multiple pages.

Trang 7

We want to show our blog’s tag cloud in multiple pages, but we don’t want to replicate the code required to fetch its items on every action of every controller that references it.SOLUTION

Action filters are classes that inherit from the infrastructural ActionFilterAttribute class and provide entry points to inject logic during the execution of an action Their base class exposes four virtual methods, listed in Table 9.1, which are automatically triggered by the ASP.NET MVC execution engine while processing a request

If you override these methods from within a custom filter class, you can inject alized logic into one or more of the well-defined phases highlighted in figure 9.8 Then you can associate that filter with individual actions or with entire controllers—in which case it will be bound to every action it holds The end result is a reusable com-ponent and no code duplication

person-For our specific needs, our LoadTagCloudAttribute has to fetch the tag cloud data from the database (as you saw in chapter 8) and store it in the model Because we want it to be applicable to many views, and, in turn, to the different models that those views will refer to, the idea is to create the IHasTagCloud interface to mark the models that provide a TagCloudItems property, as in the following listing

OnActionExecuting Runs before the controller action is triggered.

OnActionExecuted Runs just after the action concludes its execution, but before the

ActionResult it returned starts up.

OnResultExecuting This method is triggered just before the execution of the current

ActionResult OnResultExecuted The last method you can intercept It runs after the result has been executed.

Listing 9.7 IHasTagCloud and its implementation in HomepageModel

OnAction

Executing

OnResult Executing

OnAction Executed

OnResult Executed Figure 9.8 Entry points for

action filters to inject code during the request flow

Trang 8

List<TagCloudItem> TagCloudItems { get; set; }

}

public class HomepageModel : IHasTagCloud

{

public List<Post> Posts { get; set; }

public List<TagCloudItem> TagCloudItems { get; set; }

}

VB:

Friend Interface IHasTagCloud

Property TagCloudItems As List(Of TagCloudItem)

End Interface

Public Class HomepageModel

Implements IHasTagCloud

Public Property Posts As List(Of Post)

Public Property TagCloudItems As List(Of TagCloudItem)

Implements IHasTagCloud.TagCloudItems

End Class

Now it’s time to turn our gaze to the actual action filter We need to decide which of the four provided entry points better suits our needs We want to integrate the model content, so we need a model instance that’s already created and a filter that runs after the action executes However, we have to do our job before the view is created; other-wise, it wouldn’t have any data to render

Guess what? Both OnActionExecuted and OnResultExecuting will work For our needs, they’re almost equivalent, so we can pick either one We’ll choose OnResult-Executing (the reason will be unveiled shortly) The following listing shows the fil-ter’s code

B

Trang 9

Public Overrides Sub OnResultExecuting(

ByVal filterContext As ResultExecutingContext)

MyBase.OnResultExecuting(filterContext)

Dim view = TryCast(filterContext.Result, ViewResult)

If view Is Nothing Then

Return

End If

Dim model = TryCast(view.ViewData, IHasTagCloud)

If model Is Nothing Then

Return

End If

Using ctx As New BlogModelContainer

Dim service = New TagCloudService(ctx)

OnResult-One key aspect we should point out is that we could’ve just stored the tag cloud items

in the ViewData dictionary, without worrying about building an additional interface But, with some negligible additional effort, we managed to keep our views and code strongly typed, while still being able to easily support this functionality for every model we need

Fetch tag cloud data

B

Why didn’t we override OnActionExecuted instead?

One feature of our filter is that it runs only if the result is a view Limiting the result avoids unnecessary (and expensive, although we could probably cache all the stuff) roundtrips to the database in cases when the action, for example, returns a Redi-rectResult The code we just wrote would have worked exactly the same way if we placed it in the OnActionExecuted method But what if another action filter hooked that event and changed the result type? Doing our task after that phase keeps our code up-to-date, with the ultimate result returned by the action pipeline

Trang 10

With our new LoadTagCloudAttribute action filter ready, all we have to do now to let an action load the tag cloud data is to decorate it The code is shown in the follow-ing listing.

Public Function Index() As ActionResult

Using ctx As New BlogModelContainer

Dim lastPosts = ctx.PostSet.

sim-DISCUSSION

Action filters are an extremely powerful tool, not just because they allow you to avoid code duplication, but also because they contribute to keeping your action code simple and maintainable Ending up with simple code is a key factor of developing good ASP.NET MVC applications This outcome is so important that it’s worth more discus-sion; let’s focus for a moment on the result we’ve been able to achieve with the previ-ous example

We’ve built an action filter to fetch tag cloud data and used it to decorate the homepage’s Index action Doing that allowed us to have the code in the Index method, doing the specific task it was built for—fetching the most recent three posts Displaying the tag cloud is a side requirement, potentially shared across multiple

Listing 9.9 Homepage’s Index action leveraging LoadTagCloudAttribute

No reference

to tag cloud logic

B

No reference

to tag cloud logic

B

Trang 11

actions, which we isolated in a dedicated class and activated in a declarative manner when we decorated the action with an attribute We did all that without polluting the action code with any logic related to the tag cloud.

Every time you’re building a controller and you find that you’re writing code that isn’t specific to the particular request you’re handling, you should evaluate the possi-bility of building an action filter for that situation The same ASP.NET MVC framework exposes a lot of logic via action filters, like the controller’s caching primitives you’ll see in chapter 14

In conclusion, keeping your action code simple is one of the most effective ways to write good applications Besides what you just learned, ASP.NET MVC provides multi-ple entry points that you can customize to reach this ultimate goal—model binders are one of them Let’s look at those next

9.2 User input handling made smart

So far in this chapter, you’ve seen how

you can handle user input in an

ASP.NET MVC application ASP.NET

MVC can translate everything that

comes with the HTTP request into NET

objects, allowing you to work at a high

level of abstraction without having to

take care of the single items posted in

an HTML form or coming as query

string parameters

Let’s stay with our CoolMVCBlog

application and take a look at

fig-ure 9.9; it shows a page we can use to

edit blog posts

As our application stands now,

when it responds to a request for

updat-ing a blog post, it triggers an action similar to the one shown in the followupdat-ing listupdat-ing

var original = ctx.PostSet

.Where(p => p.Id == post.Id)

.Single();

Listing 9.10 Action updating a Post

Check for valid input

B

Fetching original Post

C

Figure 9.9 A screenshot from CoolMVCBlog’s Backoffice We can use this page to create and edit a post.

Trang 12

if (this.TryUpdateModel(original))

{

ctx.SaveChanges();

return this.RedirectToAction("Index"); }

}

} this.ViewData["Authors"] = AuthorsService.GetAuthors(); this.ViewData["Categories"] = CategoriesService.GetCategories(); return this.View(post); } VB: <HttpPost()> Public Function Edit(ByVal post As Post) As ActionResult If Me.ModelState.IsValid Then

Using ctx As New BlogModelContainer Dim original = ctx.PostSet

Where(Function(p) p.Id = post.Id)

Single

If Me.TryUpdateModel(original) Then

ctx.SaveChanges()

Return Me.RedirectToAction("Index")

End If

End Using

End If

Me.ViewData("Authors") = AuthorsService.GetAuthors()

Me.ViewData("Categories") = CategoriesService.GetCategories()

Return Me.View(post)

End Function

The code is rather easy to understand If the model is valid B, it fetches the post from PostSet by using its Id C, and then applies the changes coming from the form using the TryUpdateModel helper D The last step is to save it to the database E.

Although everything seems to be working in a straightforward way, the code in list-ing 9.10 suffers from two main problems:

■ Every time we have an action that modifies an entity, we’re going to replicate the same logic of loading the old version, updating it, and then saving it after checking for its correctness

■ Complex entities can’t be automatically handled by the default infrastructure The previous action, for example, can’t actually understand the categories edi-tor as we implemented it, so the collection won’t be successfully populated

In this section, you’re going to learn how you can customize the logic ASP.NET MVC uses to handle the HTTP request to solve these two problems

Custom model binders for domain entities

When we wrote the Edit action in listing 9.10, we coded a method that accepts a Post object as an argument:

Updating original Post instance

D

Saving changes

to database

E

Check for valid input

B

Fetching original Post

C

Updating original Post instance

D

Saving changes

to database

E

TECHNIQUE 55

Trang 13

public ActionResult Edit(Post post)

VB:

Public Function Edit(ByVal post As Post) As ActionResult

Unfortunately, that Post object isn’t an actual entity recognized by ADO.NET Entity Framework, and it can’t be directly used to manage its lifecycle and persistence; it’s just an instance of the same NET type, which has never been part of an EntitySet and is unknown to any ObjectContext For this reason, we had to write some code to refetch a post and update it

Wouldn’t it be awesome if we could put our hands onto a valid Entity Framework object at the beginning, one that’s connected to an object context and already updated with the user input? To do this, we must customize the way ASP.NET MVC translates the HTTP form to a NET object—more precisely, we must build our own model binder Let’s see how

When ASP.NET MVC transforms the HTTP request’s content into a NET instance, it

leverages a particular object called a model binder A model binder usually retrieves

input data from the form and interprets it to instantiate objects Figure 9.10 tizes the whole process

Building a model binder is just a matter of creating a new class that implements the IModelBinder interface and writing some code for its BindModel method:

C#:

public object BindModel(ControllerContext controllerContext,

ModelBindingContext bindingContext)

VB:

Public Function BindModel(ByVal controllerContext As ControllerContext,

ByVal bindingContext As ModelBindingContext) As Object

Figure 9.10 The model binder acts as a mediator between the HTML form and the controller,

translating the input coming from the browser into a NET object.

Trang 14

That method receives the following input parameters that represent the particular request context it’s being executed into:

■ A ControllerContext holds information related to the current request, like the HttpContext, the specific controller in charge of handling it or the routing data

■ A ModelBindingContext is specific to the binding operation and allows access

to the model being built or to the request data

The BindModel method returns an instance of object—no type is specified—which is forwarded to the executing action, in order to represent the input’s alter-ego in the ASP.NET MVC world

The idea is to customize the process by which this instance is built, creating a new model binder The new model binder will be activated each time the action parameter involves a Post type and will use the form content to retrieve a Post entity from the database and update it before delivering it to the controller Figure 9.11 shows the whole process

The workflow in figure 9.11 is supposed to have both the model binder and the action sharing the same ObjectContext instance; that’s the only way to let ADO NET Entity Framework track all the changes both actors make to the post entity and to generate the correct UPDATE query when the action finally calls its Save-Changes method

What we ultimately need is an object context to be active along the whole request

We can achieve this by using the HTTP module shown in the following listing

Text Lorem ipsum

Marco De Sanctis

ObjectContext

Figure 9.11 When ASP.NET MVC receives a form with a Post, it uses a custom model

binder to get the original post from the database and update it with the data so the controller

can easily persist it.

Trang 15

context.PostAcquireRequestState += (s, e) =>

{

CurrentContext = new BlogModelContainer(); };

context.ReleaseRequestState += (s, e) =>

{

CurrentContext.Dispose();

CurrentContext = null;

};

} public static BlogModelContainer CurrentContext

{ get {

return (BlogModelContainer) HttpContext.Current.Session[sessionKey]; }

private set {

HttpContext.Current.Session[sessionKey] = value; }

} } VB: Public Class ObjectContextModule Implements IHttpModule Public Sub Init(ByVal context As HttpApplication) Implements IHttpModule.Init AddHandler context.PostAcquireRequestState,

Sub(s, e)

If Not HttpContext.Current Is Nothing AndAlso

Not HttpContext.Current.Session Is Nothing Then CurrentContext = New BlogModelContainer()

End If

End Sub

AddHandler context.ReleaseRequestState,

Sub(s, e)

If Not HttpContext.Current Is Nothing AndAlso

Not HttpContext.Current.Session Is Nothing Then CurrentContext.Dispose()

CurrentContext = Nothing

End If

End Sub

End Sub

Public Shared Property CurrentContext As BlogModelContainer

Get

Return TryCast(HttpContext.Current.Session(sessionKey),

BlogModelContainer)

End Get

Set(ByVal value As BlogModelContainer)

HttpContext.Current.Session(sessionKey) = value

Creates ObjectContext

B

Disposes ObjectContext

C

Retrieves current context

D

Creates ObjectContext

B

Disposes ObjectContext

C

Retrieves current context

D

Trang 16

by calling its Dispose method C With our HTTP module up and running, we don’t have to worry anymore about building an object context when we’re accessing the database: there’s always one associated with each request, and we can retrieve it using the static CurrentContext property D:

■ Native types, like string, double or DateTime

■ NET objects, including our Post class

■ Collections of objects

Rather than starting from scratch, it might be worth leveraging all those elBinder built-in features What we’re going to do is build a PostModelBinder that inherits from DefaultModelBinder and customizes its BindModel method, as in the next listing

Listing 9.12 PostModelBinder’s implementation of BindModel

Checks whether to create new Post instance

Trang 17

Public Overrides Function BindModel(

ByVal controllerContext As ControllerContext,

ByVal bindingContext As ModelBindingContext) As Object

If bindingContext.Model Is Nothing Then

Let’s recall for a moment what we’re aiming to do: we have an Edit action that accepts

a Post object and we want that Post object to be retrieved from the database and ulated In other words, our custom logic must start up when there’s a new Post instance to build Band when a not null Id tells us we’re in an edit context C When that happens, we’re going to go to the database and fetch the entity, setting it as the Model for the current BindingContext D Then it’s DefaultModelBinder’s turn: in the last step, we invoke the original BindModel implementation, grabbing the values posted from the browser and putting them into the Post properties

Thanks to all this work, our controller won’t get a simple Post instance, but a real entity, already attached to the current Entity Framework’s context Our action will become much simpler—almost trivial—like the one in the following listing

Trang 18

to persist changes with Entity Framework.

Thanks to the new PostModelBinder, our action has become simpler or, better said, it works at a higher level of abstraction The infrastructure automatically takes care of realizing when there’s an update in progress and a Post must be retrieved from the database

The implementation we made is simple, and so suffers from some limitations:

■ It’s not wise to query the database so often It would be better to cache the data, temporarily storing it elsewhere

■ The model binder is specific to the Post class, but with a little more effort, we can build a more general version that can work with all entity types

Building a universal EntityModelBinder

ASP.NET MVC applies object inheritance rules to determine which model binder must execute With this in mind, we could, for example, build an EntityModelBinder that can retrieve any known entity type from the database If we then registered it for the EntityObject base class, the runtime would automatically execute it each time it encountered a class generated by Entity Framework, easily extending the behavior we discussed to all the entities in our application

Trang 19

We didn’t worry about these weak points in this example because they would’ve made the code pointlessly more complex, with the risk of losing sight of the main task: plug our custom logic into the ASP.NET MVC runtime when it comes to parse user input and translate it into NET objects We managed this superbly by reusing a lot of built-in code, thanks to the DefaultModelBinder.

Some cases are so specific that DefaultModelBinder can’t correctly interpret the data, and we need to build a new model binder from scratch The next section will show how you can accomplish even more, using this kind of customization

Building a new model binder from scratch

DefaultModelBinder does its job pretty well if data is coming from a standard form that includes only simple elements (like text boxes, drop-down lists, or check boxes) for edit-ing the object properties But if we move

to something slightly more complex, like

the category editor in figure 9.12,

every-thing comes to a grinding halt

Categories.ascx is a custom editor

template for the

IEnumerable<Cate-gory> type; it shows two list boxes and a

couple of buttons to move the

catego-ries from one list box to the other Going into too much detail about how this plate works would be a bit off-topic—you can check it out on the included samples and read its actual code For our purposes, you just need to know that because list box content isn’t posted with the HTML form, we wrote a bunch of JavaScript to populate a hidden field called values with the IDs of the categories the user selected:

1;2;5;7

ASP.NET MVC can’t do this on its own (remember, we implemented customized logic for our categories editor), but we can once again leverage the model binders’ infra-structure to hide all the details about how this kind of data travels back to the server from the browser Let’s see how

TECHNIQUE 56

Figure 9.12 To modify the categories associated with a post, we use a custom editor that ASP.NET MVC cannot interpret with any built-in model binder.

Trang 20

When we create or edit a Post, we want its Categories collection to be automatically populated with true Entity Framework entities, based on a list of IDs we receive from the request in a hidden field

SOLUTION

For this kind of task, model binders will again be a great help in encapsulating the logic needed to translate a specific kind of input in a NET object Unfortunately, this time we can’t re-use any infrastructural code like we did in technique 55 because we’re using a customized way to encode the selected categories (which DefaultModelBinder obvi-ously can’t interpret) That means we have to build a new binder from scratch, with just the IModelBinder interface as a starting point for our CategoriesModelBinder:C#:

public class CategoriesModelBinder : IModelBinder

This time, the implementation of the BindModel method works differently than it did

in the previous example As figure 9.13 shows, it works by taking an existing Category collection (probably coming from a given Post instance, but this isn’t a requirement) and modifying its content by removing or adding instances of Category objects, according to a given list of IDs

The next listing puts that logic into actual code

Category1 Category2 Cate ory3

Figure 9.13 A custom category binder gathers the IDs coming from the request and

uses them to populate an existing collection of categories accordingly.

Trang 21

Public Function BindModel(

ByVal controllerContext As ControllerContext,

ByVal bindingContext As ModelBindingContext) As Object

Implements IModelBinder.BindModel

Dim source As EntityCollection(Of Category) =

TryCast(bindingContext.Model, EntityCollection(Of Category))

If Not source Is Nothing Then

Dim fromRequest As IEnumerable(Of Category) =

Then we move our attention to the posted data, which we retrieve via a Categories method (more on this shortly) and use them to update the original collec-tion At this point, we’ve already updated the content of the original collection, so there’s

GetPosted-no need of a result; the last step is to return a null (Nothing in Visual Basic) value Now that you have an overall picture of how CategoriesModelBinder works, we can take a closer look at how we manage to retrieve the categories from the request in GetPostedCategories, whose code is shown in the following listing

to original model

B

Gets reference

to original model

B

Trang 22

var values = stringValues.Split(';');

foreach (var item in values)

{

int id = int.Parse(item);

yield return ObjectContextModule.CurrentContext

CategorySet.Where(c => c.Id == id).Single();

}

}

VB:

Private Function GetPostedCategories(

ByVal bindingContext As ModelBindingContext)

Private Function GetCategoriesFromString(

ByVal stringValues As String) As IEnumerable(Of Category)

Dim values = stringValues.Split(CChar(";"))

Dim res As New List(Of Category)

For Each item In values

One last step is still separating us from our ultimate goal—updating the original collection with the code shown in the following listing

Gets string

of IDs from Request

D

Returns category given its ID

E

Gets string

of IDs from Request

D

Returns category given its ID

E

Trang 23

private void UpdateOriginalCategories(EntityCollection<Category> source, IEnumerable<Category> fromRequest)

{

var toRemove = source

.Where(c => !fromRequest.Any(c1 => c1.Id == c.Id))

.ToList();

var toAdd = fromRequest

.Where(c => !source.Any(c1 => c1.Id == c.Id))

Private Sub UpdateOriginalCategories(

ByVal source As EntityCollection(Of Category),

ByVal fromRequest As IEnumerable(Of Category))

Dim toRemove = source

Where(Function(c) Not fromRequest

Any(Function(c1) c1.Id = c.Id))

ToList

Dim toAdd = fromRequest

Where(Function(c) Not source

Any(Function(c1) c1.Id = c.Id))

we calculated to the original collection D

As in technique 55, for ASP.NET MVC to use our custom model binder when it comes across a collection of categories, we must register it in global.asax, whose Application_start method becomes like the one in the following listing

Listing 9.16 Updating the original categories collection

Why not read directly from the Request?

Although it might be possible to manually inspect the request content using the HttpContext.Request property, ASP.NET MVC value providers help to shield you from that dependency For example, value providers theoretically allow the same code that’s in listing 9.15 to work in a different context, where values are not coming from an HttpRequest

Items in source and not in fromRequest

B

Items in fromRequest and not in source

C

Apply changes

D

Items in source and not in fromRequest

B

Items in fromRequest and not in source

C

Apply changes

D

Trang 24

Listing 9.17 Model binders setup in global.asax

Trang 25

What we’ve built in this last example, together with the one in the previous section, lets us handle the creation of a complex entity instance, plugging the whole logic into the ASP.NET MVC infrastructure We managed to create re-usable and independent components Thanks to them, we kept our actions code simple and focused on con-trollers’ requirements (like checking whether the input is valid, redirecting to a par-ticular view, or persisting changes to the database)

When you’re working on a complex application, writing the logic in the correct place is important With editor templates, you can define how an editor for a certain type looks, and with model binders you can bridge the gap between the request that editor produces and the actual NET objects your controllers will receive

Thanks to these notions, integrating an ASP.NET MVC application with ADO.NET Entity Framework (or another persistence layer) should be easier Now, though we’ll remain in the field of ASP.NET MVC customizations, we’re definitely going to change topics We’ll explore how you can optimize the default routing infrastructure to improve search engine indexing of your web sites

9.3 Improving ASP.NET MVC routing

We introduced routing in ASP.NET MVC in chapter 8 In that chapter, you discovered the central role it plays in this web development technology in mapping URLs to actions and controllers

Routes are a great and effective way to improve URL readability, and ASP.NET MVC natively sets up a routing scheme that avoids query string parameters where possible, giving the application URLs a static look Unfortunately, the standard functionality has

a weak point, but we can correct it to significantly improve the search engine ranking

of our pages In this section, you’ll discover how

Routes with consistent URL termination

ASP.NET routing is robust when it’s parsing URLs to determine which controller will handle the request and what parameters it will receive For example, it doesn’t impose any rule for how the address has to be terminated If we’re using the default {control-ler}/{action}/{id} schema, it will successfully tokenize URLs like the following as if they were the same one:

■ Home/Post/3

■ Home/Post/3/

This feature makes it easy to avoid schema proliferation because both these URLs are valid and both need to be supported, but it raises a problem when it comes time to improve page rankings: they are different URLs, and this causes all the visits to be split among the two

PROBLEM

You want to raise your web site search engine rank, so you have to avoid link duplication You’re going to add a trailing slash to your links and flag the ones that lack it as invalid

TECHNIQUE 57

Ngày đăng: 12/08/2014, 15:23

TỪ KHÓA LIÊN QUAN