Thursday, 1 May 2008

Firing Static Events from Instance Methods in C#

In a current project, I have a UserDocument class, which inherits from EditableBase<T>, and stores metadata about the contents of a file that's stored somewhere on my app server. What I needed to do was to add an event to the business object that would fire just after deleting the UserDocument's record, so that I could delete the corresponding file from the server's filesystem. The solution is almost what I expected, with one slightly odd workaround.

OK, the original code looked (very roughly) like this. We're using generics throughout so T denotes "whatever kind of business object this is" - Customer, Invoice, whatever.

// Our event handler methods have to match the following delegate
public delegate void DataEventHandler<T>(T t);

// The base class for our editable business object.
public class EditableBase<T> {

// The event we want to fire immediately after deleting a database record.
public static event DataEventHandler<T> DeletingRecord;

// The event we want to fire immediately after deleting a database record.
public static event DataEventHandler<T> DeletedRecord;

}

// A class representing a document or file that's been uploaded to our
// application by a user.
public class UserDocument : EditableBase<UserDocument> {

private string filename;
public string Filename {
get { return filename; }
set { filename = value; }
}

public void Delete() {
// this is the call to the DAL to actually remove the record
// from the UserDocument table.
DataContext.Current.DeleteUserDocument(filename);
}
}

First approach - let's just fire the event in the usual way:

public void Delete() {
    // this is the call to the DAL to actually remove the record 
    // from the UserDocument table.
    DataContext.Current.DeleteUserDocument(filename);
    if (DeletedRecord != null) DeletedRecord(this);
}

Erk. Compiler doesn't like that...

The event 'EditableBase<UserDocument>.DeletedRecord' can only appear on the left hand side of += or -= (except when used from within the type 'EditableBase<T>')

It would appear that you can't fire static events from instance methods. No idea why this is the case - I can't see any reason for it - but the workaround is pretty straightforward. First, we provide a static wrapper method for each of our EditableBase<T> events:

// A wrapper method that can 'see' the static event, but can be called    
// from instance methods.
public static voidNotifyDeletedRecord(T t) {
    if(DeletedRecord != null) DeletedRecord(t);
}

What's cool about this method is that it provides an adapter between instance methods and static events. Instance methods can call static methods; static methods can fire static events. Our instance method can now quite happily do this:

public void Delete() {
// this is the call to the DAL to actually remove the record
// from the UserDocument table.
DataContext.Current.DeleteUserDocument(filename);
NotifyDeletedRecord(this);
}

and then the NotifyDeletedRecord method will raise the static event, and any handlers are attached to it will fire as expected.

What's cool about this particular example is the ability to do this sort of thing - this code is from global.asax, and shows clearly how we can bind a very simple event handler to the static event on the UserDocument class that will clean up the underlying files whenever a record is removed:

namespace MyProject.Website {
public class Global : System.Web.HttpApplication {

protected void Application_Start(object sender, EventArgs e) {
// Attach event handler that cleans up files on disk
// after DB records are deleted.
UserDocument.DeletedRecord +=
new DataEventHandler<UserDocument>(DeleteUserDocumentFile);
}

// This method removes the file associated with the specified UserDocument
// from the application servers' filesystem.

void DeleteUserDocumentFile(UserDocument t) {
string userPath = HttpContext.Current.Server.MapPath("~/UserDocuments/");
string filePath = Path.Combine(userPath, t.Filename);
if (File.Exists(filePath)) {
try {
File.Delete(filePath);
} catch (Exception ex) {
// Something went wrong - maybe log the error, or add
// to a queue of files to be manually cleaned up later?
// In the meantime, just throw the exception again.
throw (ex);
}
}
}
}
}

But why not just put the File.Delete() inside the UserDocument class?

Because, depending on the context in which our business objects are running, we could be deleting from the local filesystem (via Server.MapPath() because we're a website), or via a WebDAV call to a remote file server, or by calling some web service that deletes the remote file for us. Separating the requirement and the implementation in this way means our business objects implement deletion consistently and our application itself is free to run cleanup code.

I think it's quite a nice approach - we're now using it with NotifyCreating, NotifyInserting / NotifyInserted, and all sorts of other hooks around CRUD data access methods, and it seems to be working really rather nicely.

1 comment:

Stevan Rodrigues said...

Please check this blog.
http://blog.monstuff.com/archives/000040.html

You are invoking the delete event in the sub class. Events can be invoked in the class that defines it.