ActiveRecord, Caliburn Micro and one line of code
Labels: activerecord, caliburn, wpf
For some months ago I started a WPF project for my employer. The application that is meant to be the result of the project will include simple record viewing and editing.
After some research, I choose to use Castle ActiveRecord for database interaction and Caliburn Micro’s implementation of the MVVM (Model-View-ViewModel) pattern. As far as the record viewing and editing part is concerned, this turned out to be a lucky choice. Besides from building the Model and drawing a View in design mode, the ViewModel has exactly one line of code!
Caliburn Micro’s MVVM implementation is built on a “Model first” principle, which basically means that you let your code load a ViewModel and the framework will load the related view. When nothing else is specified, “MyFirstViewModel” will search for and load “MyFirstView”. Of course you can connect your ViewModel to more, or another View – but that’s not interesting for this article.
Let’s take the person registration as example. As simple form where you enter a name, address, phone, etc. First, we create the Model and apply the ActiveRecord attributes to it:
[ActiveRecord]
public class Person
: ActiveRecordBase<Person>
{
[PrimaryKey]
public int PersonId { get; set; }
[Property]
public string FirstName { get; set; }
[Property]
public string LastName { get; set; }
// add more properties here ...
}
As you can see, the Person object only contains properties, no methods. The ActiveRecordBase base class contains methods for database CRUD operations and for this object, no additional methods are required.
Next, we’ll build the view in XAML. For the sake of simplicity, styles and other GUI related expressions have been omitted from the code below. Notice the link to Caliburn Micro in the header of the UserControl.
<UserControl x:Class="PersonView"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cal="http://www.caliburnproject.org">
<Grid Background="Transparent" DataContext="{Binding Path=Person}">
<StackPanel>
<StackPanel Orientation="Horizontal">
<Label Content="Firstname" />
<TextBox Text="{Binding FirstName}" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label Content="Lastname" />
<TextBox Text="{Binding LastName}" />
</StackPanel>
</StackPanel>
<Button Content="Save" cal:Message.Attach="Save()" />
</Grid>
</UserControl>
Last – but not least – the ViewModel. Now, the code in the ViewModel is actually what this article is about. Not because it’s huge or complex – rather because it’s almost too simple. See here:
using Caliburn.Micro;
public class PersonViewModel : Screen
{
public Person Person { get; set; }
}
That’s all folks!
So, how can you actually create or edit records here that’ll be persisted in the database? The trick is the intelligence of Caliburn Micro with the base method of ActiveRecord.
In part 3 of the “Soup to Nuts” series of articles that Rob Eisenberg wrote about Caliburn Micro1, he explains how the button’s default event (the Click event) is used when no other event is specified. That’s how the Save() method is called when the Button at the bottom of the XAML code is clicked.
Finally, there’s this little footnote in the same article that does the trick:
Actually, if no handler is found, before an exception is thrown, the framework will check the current DataContext to see if it has the requested method.
So, when my ViewModel doesn’t have any Save() method specified, Caliburn Micro will check the DataContext (read: the Person object instance) for this method. Now, the Person object doesn’t have this method either, but its base method – ActiveRecordBase – does! And that’s why I don’t need to write more than one line of code in my ViewModel: the only thing I need to tell the View is that its DataContext is a Person object.
Another thing with Caliburn Micro is that it instantiates a new object instance of the DataContext whenever the UserControl (or Window) is opened and the DataContext isn’t assigned. So, when creating a new Person, I don’t have to instantiate it in my code. Again, that leaves less code for me to write. The only thing that’s left is to find the right Person when you need to edit.
public void EditPerson(int personId)
{
Person = Person.FindOne(Restrictions.Eq("PersonId", personId));
}
Building your application won’t get much simpler, will it?
Note:
In this article, I’ve tried to explain a concept, which not necessarily means that the article in its whole or any part of it – including the code samples – will give a ready-to-run application when applied. The purpose of this article is to understand how the concept works and how you could apply this concept in your project. I’m aware that the sample code is fairly limited, but in my opinion this can easily be extended once you understand the concept.
Using IHttpAsyncHandler and XMLHttpRequest to “push” messages to the client
I've been playing around with “comet” a little and trying to make it work in ASP.NET without modifying anything in the IIS. There are a few web servers or IIS enhancements available that provide comet functionality, but they require you to have control over IIS or even over the complete system allowing you to replace IIS with a proprietary web server. But you might want to use comet in a shared or hosted environment, where modifying the web server is not an option.
The solution I found - so far - was inspired by the reactions on Aaron Lerch's blog post. Aaron is definitely in to something when he writes his article, but the big issue with his sample is that the code doesn't scale to well, as he writes. The responses suggest using asynchronous handling of the requests, and there's even a link to a Comet-enabled GridView on CodeProject. That GridView worked for me, but it was far too complicated for what I wanted: simply pushing a message to a user that is on my website.
Here's what I did:
I started with a simple “default.aspx” in a normal web site. This page will call the async handler using the XMLHttpRequest object (well-known from AJAX implementations).
When a user visits the website, we’ll somehow have to let the server know that he’s there. There are a number of ways to solve this; I chose to create a List of Session IDs and add the users’ session id to the list in the Session_Start event handler in Global.asax. Of course, there are better solutions, but it’ll do for the purpose of this example.
public class Global : System.Web.HttpApplication
The solution I found - so far - was inspired by the reactions on Aaron Lerch's blog post. Aaron is definitely in to something when he writes his article, but the big issue with his sample is that the code doesn't scale to well, as he writes. The responses suggest using asynchronous handling of the requests, and there's even a link to a Comet-enabled GridView on CodeProject. That GridView worked for me, but it was far too complicated for what I wanted: simply pushing a message to a user that is on my website.
Here's what I did:
I started with a simple “default.aspx” in a normal web site. This page will call the async handler using the XMLHttpRequest object (well-known from AJAX implementations).
When a user visits the website, we’ll somehow have to let the server know that he’s there. There are a number of ways to solve this; I chose to create a List of Session IDs and add the users’ session id to the list in the Session_Start event handler in Global.asax. Of course, there are better solutions, but it’ll do for the purpose of this example.
public class Global : System.Web.HttpApplication
{
public static List<string> Sessions;
protected void Application_Start(object sender, EventArgs e)
{
Sessions = new List<string>();
}
protected void Session_Start(object sender, EventArgs e)
{
if (!Sessions.Contains(Session.SessionID))
Sessions.Add(Session.SessionID);
}
protected void Session_End(object sender, EventArgs e)
{
if (Sessions.Contains(Session.SessionID))
Sessions.Remove(Session.SessionID);
}
}
We need a message handler on the server that will deliver the message to the right session. I created a synchronous handler. Since it handles the messages immediately, it doesn’t hold on any threads or resources after the message is delivered.
///
We need a message handler on the server that will deliver the message to the right session. I created a synchronous handler. Since it handles the messages immediately, it doesn’t hold on any threads or resources after the message is delivered.
///
/// An IHttpHandler for the messages that are sent from a session
///
public class MyMessageHandler : IHttpHandler
{
#region IHttpHandler Members
public bool IsReusable
{
get { return true; }
}
public void ProcessRequest(HttpContext context)
{
// Find the handle in the queue, identified by its session id
var recipient = context.Request["recipient"];
var handle = MyAsyncHandler.Queue.Find(q => q.SessionId == recipient);
// just a small check to prevent NullReferenceException
if (handle == null)
return;
// Dump the message in the handle;
handle.Message = context.Request["message"];
// Set the handle to complete, this triggers the callback
handle.SetCompleted(true);
}
#endregion
}
Next is the asynchronous handler to handle requests from the XMLHttpRequest object on the Default.aspx page and deliver a message back. I called it MyAsyncHandler and let it inherit from the System.Web.IHttpAsyncHandler interface. There’s some logic there and I decided to just explain the most important parts. First of all, the static constructor initializes the queue (a System.Collections.Generic.List) that is intended to hold all the asynchronous results.
Next is the asynchronous handler to handle requests from the XMLHttpRequest object on the Default.aspx page and deliver a message back. I called it MyAsyncHandler and let it inherit from the System.Web.IHttpAsyncHandler interface. There’s some logic there and I decided to just explain the most important parts. First of all, the static constructor initializes the queue (a System.Collections.Generic.List) that is intended to hold all the asynchronous results.
Next, when the request comes in it calls the BeginProcessRequest method. This method checks if the session already was registered in the queue before or if it should add the session to the queue.
Finally, the EndProcessRequest uses its result parameter (of type MyAsyncResult) and finds the HttpContext object in there. Using the HttpContext.Response.Write, it “pushes” the message to the session that is intended to be the recipient of the message, identified by its session id.
///
///
/// An IHttpAsyncHandler to "push" messages to the intended recipients
///
public class MyAsyncHandler : IHttpAsyncHandler
{
///
/// The queue holds a list of asynchronous results with information about registered sessions
///
public static List<MyAsyncResult> Queue;
///
/// Static constructor
///
static MyAsyncHandler()
{
// Initialize the queue
Queue = new List<MyAsyncResult>();
}
#region IHttpAsyncHandler Members
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
{
// Fetch the session id from the request
var sessionId = context.Request["sessionId"];
// Check if the session is already registered
if (Queue.Find(q => q.SessionId == sessionId) != null)
{
var index = Queue.IndexOf(Queue.Find(q => q.SessionId == sessionId));
// The session has already been registered, just refresh the HttpContext and the AsyncCallback
Queue[index].Context = context;
Queue[index].Callback = cb;
return Queue[index];
}
// Create a new AsyncResult that holds the information about the session
var asyncResult = new MyAsyncResult(context, cb, sessionId);
// This session has not been registered yet, add it to the queue
Queue.Add(asyncResult);
return asyncResult;
}
public void EndProcessRequest(IAsyncResult result)
{
var rslt = (MyAsyncResult) result;
// send the message to the recipient using the recipients HttpContext.Response object
rslt.Context.Response.Write(rslt.Message);
// reset the message object
rslt.Message = string.Empty;
}
#endregion
#region IHttpHandler Members
public bool IsReusable
{
get { return true; }
}
///
/// In an asychronous solution, this message shouldn't be called
///
public void ProcessRequest(HttpContext context)
public void ProcessRequest(HttpContext context)
{
throw new NotImplementedException();
}
#endregion
}
As soon as the client gets a response, the XMLHttpRequest object notices a “ready state” change and the “onreadystatechange” handler kicks in. The message is parsed into an object on the page (e.g. a DIV) and directly thereafter, the page sends a new asynchronous request, signaling the server that it's ready to receive another message.
Don’t forget to register the handlers in the Web.config. You web application needs to know what classes handle the requests from the clients. Add the following lines to the system.web/httpHandlers section:
As soon as the client gets a response, the XMLHttpRequest object notices a “ready state” change and the “onreadystatechange” handler kicks in. The message is parsed into an object on the page (e.g. a DIV) and directly thereafter, the page sends a new asynchronous request, signaling the server that it's ready to receive another message.
Don’t forget to register the handlers in the Web.config. You web application needs to know what classes handle the requests from the clients. Add the following lines to the system.web/httpHandlers section:
NB! If you’re using IIS7, you add these lines to the system.webserver/handlers section instead.
<add verb="GET,POST" path="MyAsyncHandler.ashx" type="SandBox.CometSample.MyAsyncHandler, SandBox.CometSample" validate="false"/>
<add verb="GET,POST" path="MyAsyncHandler.ashx" type="SandBox.CometSample.MyAsyncHandler, SandBox.CometSample" validate="false"/>
<add verb="GET,POST" path="MyMessageHandler.ashx" type="SandBox.CometSample.MyMessageHandler, SandBox.CometSample" validate="false"/>
The Default.aspx looks as follows:
<html xmlns="http://www.w3.org/1999/xhtml">
The Default.aspx looks as follows:
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>title>
<script type="text/javascript">
function init() {
var send = document.getElementById('btnSend');
if (!send.addEventListener) {
send.addEventListener = function(type, listener, useCapture) {
attachEvent('on' + type, function() { listener(event) });
}
}
send.addEventListener('click', function() { send(); }, false);
hook();
}
function hook() {
var url = 'MyAsyncHandler.ashx?sessionId=';
var request = getRequestObject();
request.onreadystatechange = function() {
try {
if (request.readyState == 4) {
if (request.status == 200) {
document.getElementById('incoming').innerHTML += request.responseText + '
';
';
// immediately send a new request to tell the async handler that the client is
// ready to receive new messages;
hook();
}
else {
document.getElementById('incoming').innerHTML += request.responseText + '
';
';
}
}
}
catch (e) {
document.getElementById('incoming').innerHTML = "Error: " + e.message;
}
};
request.open('POST', url, true);
request.send(null);
}
function send() {
var message = document.getElementById('message').value;
var recipient = document.getElementById('').value;
var request = getRequestObject();
var url = 'MyMessageHandler.ashx?message=' + message + '&recipient=' + recipient;
var params = 'message=' + message + '&recipient=' + recipient;
document.getElementById('incoming').innerHTML += '' + message + '
';
';
request.onreadystatechange = function() {
if (request.readyState == 4 && request.status != 200)
alert('Error ' + request.status + ' trying to send message');
};
request.open('POST', url, true);
request.send(params);
}
function getRequestObject() {
var req;
if (window.XMLHttpRequest && !(window.ActiveXObject)) {
try {
req = new XMLHttpRequest();
}
catch (e) {
req = false;
}
}
else if (window.ActiveXObject) {
try {
req = new ActiveXObject('Msxml2.XMLHTTP');
}
catch (e) {
try {
req = new ActiveXObject('Microsoft.XMLHTTP');
}
catch (e) {
req = false;
}
}
}
return req;
}
script>
head>
<body onload="setTimeout('init();', 500);">
<form id="form1" runat="server">
<div>
Self: <asp:Literal ID="ltlSessionId" runat="server" /><br />
Message: <input type="text" id="message" /><br />
Recipient: <asp:DropDownList ID="ddlSessions" runat="server" />
<br />
<input type="button" id="btnSend" value="Send Message!" onclick="send();" />
<hr />
<div id="incoming">
div>
div>
form>
body>
html>
The only C# code for this page is to populate the DropDownList with the sessions.
public partial class Default : System.Web.UI.Page
The only C# code for this page is to populate the DropDownList with the sessions.
public partial class Default : System.Web.UI.Page
{
protected void Page_Init(object sender, EventArgs e)
{
if (IsPostBack)
return;
ltlSessionId.Text = Session.SessionID;
foreach (var sessionId in Global.Sessions)
if (sessionId == Session.SessionID)
ddlSessions.Items.Add(new ListItem("Myself", sessionId));
else
ddlSessions.Items.Add(new ListItem(sessionId, sessionId));
}
}
There are – at least – three things with this example that you immediately might want to do better. First of all, sending a message to a Session ID is pretty vague. You’d let the website visitor enter his name so that other users can send messages to a name rather than a Session ID.
Next, this example doesn’t check if the Session is still active. The Session might have timed-out or abandoned, so that the Session_End method in the Global.asax never got called. To deal with this, you should implement a neat solution to keep track of Sessions.
Last but not least: you’ve to refresh your website manually to check if new sessions are available. You could use the same Comet technique to update the DropDownList automatically when a new visitor enters the website. Or you could use the “old fashioned” AJAX way and poll your page periodically to update the list.
The source code was created in Visual Studio 2008 with .NET 3.5 and can be downloaded here
There are – at least – three things with this example that you immediately might want to do better. First of all, sending a message to a Session ID is pretty vague. You’d let the website visitor enter his name so that other users can send messages to a name rather than a Session ID.
Next, this example doesn’t check if the Session is still active. The Session might have timed-out or abandoned, so that the Session_End method in the Global.asax never got called. To deal with this, you should implement a neat solution to keep track of Sessions.
Last but not least: you’ve to refresh your website manually to check if new sessions are available. You could use the same Comet technique to update the DropDownList automatically when a new visitor enters the website. Or you could use the “old fashioned” AJAX way and poll your page periodically to update the list.
The source code was created in Visual Studio 2008 with .NET 3.5 and can be downloaded here