This is post #15 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.
The ying to register's yang, I need to let users modify their registration details through a profile screen.
The update story is very similar to the create story (see the register actions). Normally the only differences are:
An additional difference here is there is no return URL, so I'm redirecting back to the profile action to let the user make another change if they need to. It's important to redirect after a successful postback action so that a browser refresh doesn't resubmit. It doesn't matter where the redirect goes, the important part is that a redirect happens.
[Registered]
public ActionResult Profile()
{
User user = Container.Get<IUserService>().GetCurrentUser();
return View(user);
}
[HttpPost]
[Registered]
public ActionResult Profile(FormCollection collection)
{
User user = Container.Get<IUserService>().GetCurrentUser();
try
{
TryUpdateModel(user, new[] { "Version", "DisplayName", "Email", "Website" });
if (!ModelState.IsValid)
return View(user);
Container.Get<IUserService>().UpdateUser(user);
Messages.Add("Profile updated successfully.");
return RedirectToAction("profile");
}
catch (Exception ex)
{
Messages.Add(ex);
return View(user);
}
}
Notice the object is re-retrieved from the database in the postback action. It's not serialised or stored anywhere by the get action, apart from displaying some of its details on the screen. This is generally good practice -- serialisation to the UI might be a security risk, and session storage might expire. Re-retrieving the object is more in line with the disconnected stateless reality of web applications.
To compensate, the version of the object has to go into a hidden input in the form to keep optimistic concurrency alive. If the version isn't included in the form and isn't set in the postback action, optimistic concurrency is dead and changes can be overwritten without users seeing them. This is the real weakness of this approach -- remembering to include and set the version on every update and delete form.
There's a bug here. Suppose I put a bad value into my profile screen, like a really long display name.
See the bad display name in the top right hand corner of the screen? That's because I'm working on the same user object the rest of the screen is using -- which probably isn't a good idea. I don't want to affect the current user object until the changes have been validated and saved to the database. What I really need here is some way to work on a copy of the current user object.
In NHibernate there is the concept of eviction from the session. Normally each object hangs on to its session for lazy loading, and the session hangs on to each object for caching. Eviction severs the relationship between the two completely and is exactly what I need here. That will give me an instance of the current user object that no other part of the application will know about it. When they ask for the current user object, NHibernate will see it doesn't have one in cache and will instantiate a new one. Wait, will they have the copy or will I? Neither, we'll both just have separate instances of the same object.
This is only an issue because I'm hooked into my domain model directly. If I was using a view model, this wouldn't be a problem.
Hooking directly into NHibernate in my controller to evict something would be bad form. This needs to belong somewhere generic instead -- like IContext
.
namespace StackingCode.Moja.Repositories
{
public interface IContext
{
ITransaction BeginTransaction();
ITransaction BeginTransaction(IsolationLevel isolationLevel);
void Evict(object entity);
}
}
You could argue hooking into IContext
in my controller is also bad form, and you'd be right. But it's less so than hooking into NHibernate -- and my imagination is failing me, so it will do for now.
The NHibernate implementation of IContext
looks just like this.
namespace StackingCode.Moja.Repositories.NHibernate
{
public class Context : IContext
{
protected ISession Session { get; set; }
#region IContext Members
// ...
public void Evict(object entity)
{
Session.Evict(entity);
}
#endregion
}
}
Now I can modify my controller to work on a separate instance of the current user object when it needs to.
[HttpPost]
[Registered]
public ActionResult Profile(FormCollection collection)
{
User user = Container.Get<IUserService>().GetCurrentUser();
try
{
TryUpdateModel(user, new[] { "Version", "DisplayName", "Email", "Website" });
if (!ModelState.IsValid)
{
// Isolate this instance.
Container.Get<IContext>().Evict(user);
return View(user);
}
Container.Get<IUserService>().UpdateUser(user);
Messages.Add("Profile updated successfully.");
return RedirectToAction("profile");
}
catch (Exception exception)
{
// Isolate this instance.
Container.Get<IContext>().Evict(user);
Messages.Add(exception);
return View(user);
}
}
Which lets me safely enter crappy data and not have it affect the rest of the screen.
Yes it is. In summary...
Just a couple things to keep in mind.
EDIT: A later refactor with the use of view models eliminated the need for IContext.Evict
.
There are 0 comments.
Older
Speed Bumps
Older
Speed Bumps
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.