Register | Login

Stacking Code

public interface IBlog { string Dump(Stream consciousness); }

Another View Model Refactor for Attachments

Saturday, 18 December, 2010 @ 6:24 AM < Adam Boddington
Tags: Architecture, ASP.NET MVC, Building Neno, NHibernate

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.

  1. Another View Model Refactor for Attachments talks about the differences between view models as adapters and view models as containers, and why I had to switch.
  2. Files in Databases talks about the compelling advantages to storing files in databases and blindly ignores the disadvantages. ;)
  3. BLOBs, NHibernate and SQL Server talks about some of the technical details of putting binary large objects in SQL Server with NHibernate.

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.

View Models as Adapters

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.

View Models as Containers

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.

In Action

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.

Evict Evict

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.


Comments

Leave a Comment

Please register or login to leave a comment.


Older
Pages

Newer
Files in Databases

Older
Pages

Newer
Files in Databases

browse with Pivot


About


Projects

Building Neno


RSS
Recent Posts

Codility Nitrogenium Challenge
OS X Lock
HACT '13
Codility Challenges
Priority Queue


Tags

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)


Archives


Powered by Neno, ASP.NET MVC, NHibernate, and small furry mammals. Copyright 2010 - 2011 Adam Boddington.
Version 1.0 Alpha (d9e7e4b68c07), Build Date Sunday, 30 January, 2011 @ 11:37 AM