This is post #25 in the Building Neno series. Please click here for a description of the Building Neno project and instructions on how to access the source code for this post.
This post turned into a monster, so I had to break it up into three smaller posts.
Onwards and upwards.
In my first crack at view models I set them up as little more than domain model adapters -- with properties passing through to domain object properties, sometimes doing a little transformation on the way. In building out attachments I realised the limitation of that approach.
Here's an example. Let's say I'm editing a recent post. The view model for that action currently looks like this.
public class Edit : PostView
{
public Edit(DomainModel.Post post)
: base(post)
{
}
public int Version
{
get { return Post.Version; }
set { Post.Version = value; }
}
// ...
[Required]
[StringLength(100)]
public string Title
{
get { return Post.Title; }
set { Post.Title = value; }
}
// ... and so on
}
If a user puts in a bad title value, like an empty string, the view model collects it and passes it immediately to the domain object. As a result, I don't get a friendly "The Title field is required" message back. Instead I get an actual error because my recent posts partial view in the sidebar is using the same domain object and can't use the title to do the action link. (The link text can't be an empty string.)
I've run into this sort of problem before when I was doing the user profile view. The solution back then was to evict the object from the NHibernate session and force the application to have two instances of the object in memory. One for editing, one for displaying. I wasn't particularly happy about the solution because it exposed some of NHibernate's object management to the UI. Knowing how NHibernate works should really start and end in the StackingCode.Neno.Repositories.NHibernate
namespace as much as possible.
But that was the solution then, and to be consistent I had to do it the same way again now. The bad news is that eviction won't actually save me this time around. If I evict my post domain object here, I avoid my recent posts partial view error, but I get a new error because the view is trying to access lazy loaded properties at render time. Properties like comments and tags, which it can't access because the session is lost to the object when eviction occurs.
I could eagerly load those properties before eviction, but again, more knowledge of NHibernate's object management may creep into the UI as a result. Do I eagerly load the properties in the controller, does the controller ask the service/repository to do it, or does the repository just do it all the time? The third one keeps the controller ignorant, but it's not really viable since that particular garden path may one day see me pulling my entire object graph with a single database call.
That's the bad news. The good news is that I now have an opportunity to get rid of the ugliness of IContext.Evict
if I can solve this problem another way.
The idea here is to let views models have their own data rather than being live adapters to domain objects all the time. They can still fulfil an adapter role when it comes to loading and saving -- but in between those two events the data is the responsibility of the view models to display, collect, and validate.
Here's what the post edit view model looks like now.
public class PostEdit : PostViewModel
{
public PostEdit(Post post)
: base(post)
{
CopyFromPost();
}
public int Version { get; set; }
// ...
[Required]
[StringLength(100)]
public string Title { get; set; }
// ...
public void CopyToPost()
{
Post.Version = Version;
// ...
Post.Title = Title;
// ... and so on
}
private void CopyFromPost()
{
Version = Post.Version;
// ...
Title = Post.Title;
// ... and so on
}
}
Besides properties for its own data, I've included some utility methods for copying that data to and from the domain object when the time is right. It isn't visible above, but the view model still has a post domain object attached as a property. That's for all the other data needed for display only -- like tags, comments, and (soon to be) attachments. It didn't make sense to copy that data into the view model as well when it's already easily consumable for display purposes.
Here's a quick example of how the actions have changes as a result.
[UserIsTheAuthorOrAnAdministrator("Id")]
public ActionResult Edit(int id)
{
// I can assume the post exists or the author check above would've redirected me away.
Post post = Container.Get<IPostService>().GetPost(id);
var model = new PostEdit(post);
return View(model);
}
[HttpPost]
[UserIsTheAuthorOrAnAdministrator("Id")]
[ValidateInput(false)]
public ActionResult Edit(int id, FormCollection collection)
{
// I can assume the post exists or the attribute above will redirect me away.
Post post = Container.Get<IPostService>().GetPost(id);
var model = new PostEdit(post);
try
{
TryUpdateModel(model, new[] { "Version", "PublishDateTimeOffset", "IsPublished", "Title", "Slug", "Text", "Tags" });
if (!ModelState.IsValid)
return View(model);
model.CopyToPost();
Container.Get<IPostService>().UpdatePost(post);
Messages.Add("Post updated successfully.");
return RedirectToAction("edit", "post", new { post.Id });
}
catch (Exception exception)
{
Messages.Add(exception);
return View(model);
}
}
The view model looks after display, collection and validation. The domain object stays intact until right before the update operation when the data is copied across. Invalid data (like empty titles) won't affect anything other than the view model.
So I've isolated invalid data to view models, removing the need for IContext.Evict
in the above action. By going back to the user profile actions and applying the same concept there too, I can get rid of IContext.Evict
altogether. Which really was an unnecessary bit of complication.
Things are working a bit more elegantly now.
There are 0 comments.
Older
Pages
Newer
Files in Databases
Older
Pages
Newer
Files in Databases
browse with Pivot
Codility Nitrogenium Challenge
OS X Lock
HACT '13
Codility Challenges
Priority Queue
Architecture (13)
ASP.NET (2)
ASP.NET MVC (13)
Brisbane Flood (1)
Building Neno (38)
C# (4)
Challenges (3)
Collections (1)
Communicator (1)
Concurrency Control (2)
Configuration (1)
CSS (5)
DataAnnotations (2)
Database (1)
DotNetOpenAuth (2)
Entity Framework (1)
FluentNHibernate (2)
Inversion of Control (5)
JavaScript (1)
jQuery (4)
Kata (2)
Linq (7)
Markdown (4)
Mercurial (5)
NHibernate (20)
Ninject (2)
OpenID (3)
OS X (1)
Pivot (6)
PowerShell (8)
Prettify (2)
RSS (1)
Spring (3)
SQL Server (5)
T-SQL (2)
Validation (2)
Vim (1)
Visual Studio (2)
Windows Forms (3)
Windows Service (1)
Comments
Leave a Comment
Please register or login to leave a comment.