MonoRail: Using DynamicActions and DynamicActionProviders

November 4th, 2006

This is one piece of MonoRail that, albeit is documented for a long time, is not explored and I think most people haven’t even heard about it.

DynamicActions offers a way to have an action that is not a method in the controller. This way you can “reuse an action” in several controllers, even among projects, without the need to create a complex controller class hierarchy. A dynamic action is defined by implementing the interface IDynamicAction.

A Dynamic Action Provider is a way to attach dynamic actions to a controller. You can associate one or more action provider with a controller, they will be invoked by the framework and will have a change to include the dynamic action it wants.

To illustrate its usage, support you have several controllers that are nothing but CRUD controllers. Picking one of these:

[Layout("default"), Rescue("generalerror")]
public class CategoryController : ARSmartDispatcherController
{
    public void List()
    {
        PropertyBag["items"] = Category.FindAll();
    }
    
    public void New()
    {
    }
    
    [AccessibleThrough(Verb.Post)]
    public void Create([ARDataBind("category", AutoLoadBehavior.OnlyNested)] Category category)
    {
        try
        {
            category.Create();

            RedirectToAction("list");
        }
        catch(Exception ex)
        {
            Flash["errormessage"] = ex.Message;
            Flash["category"] = category;

            RedirectToAction("new");
        }
    }
    
    public void Edit(int id)
    {
        if (!Flash.Contains("category"))
        {
            PropertyBag["category"] = Category.Find(id);
        }
    }

    [AccessibleThrough(Verb.Post)]
    public void Update([ARDataBind("category", AutoLoadBehavior.Always)] Category category)
    {
        try
        {
            category.Create();

            RedirectToAction("list");
        }
        catch (Exception ex)
        {
            Flash["errormessage"] = ex.Message;
            Flash["category"] = category;

            RedirectToAction("edit", "id" + category.Id);
        }
    }
    
    public void ConfirmDelete([ARFetch("id", false, true)] Category category)
    {
        PropertyBag["category"] = category;
    }
    
    [AccessibleThrough(Verb.Post)]
    public void Delete([ARFetch("category.id", false, true)] Category category)
    {
        try
        {
            category.Delete();

            RedirectToAction("list");
        }
        catch(Exception ex)
        {
            Flash["errormessage"] = ex.Message;

            RedirectToAction("confirmdelete", "id" + category.Id);
        }
    }
}

Now, copying and pasting this code to several controllers is not a good approach. What shall we do?

You can translate each action to a dynamic action and have a dynamic action provider configuring them. Kind of advanced usage, but nevertheless you should be aware of it.

In my example I created an attribute CrudAttribute that is used to expose information to my dynamic action provider. So the controller above will look like the following:

[Crud(typeof(Category))]
[DynamicActionProvider(typeof(CrudActionProvider))]
[Layout("default"), Rescue("generalerror")]
public class CategoryController : ARSmartDispatcherController
{    
}

And it would be easy to create new ones:

[Crud(typeof(User))]
[DynamicActionProvider(typeof(CrudActionProvider))]
[Layout("default"), Rescue("generalerror")]
public class UserController : ARSmartDispatcherController
{    
}

[Crud(typeof(Account))]
[DynamicActionProvider(typeof(CrudActionProvider))]
[Layout("default"), Rescue("generalerror")]
public class AccountController : ARSmartDispatcherController
{    
}

Now, what does the CrudActionProvider look like?

public class CrudActionProvider : IDynamicActionProvider
{
    public void IncludeActions(Controller controller)
    {
        Type controllerType = controller.GetType();

        object[] atts = controllerType.GetCustomAttributes(typeof(CrudAttribute), false);

        if (atts.Length == 0)
        {
            throw new Exception("CrudAttribute not used on " + controllerType.Name);
        }

        CrudAttribute crudAtt = (CrudAttribute) atts[0];

        Type arType = crudAtt.ActiveRecordType;
        String prefix = arType.Name.ToLower();

        controller.DynamicActions["list"] = new ListAction(arType);
        controller.DynamicActions["new"] = new NewAction();
        controller.DynamicActions["create"] = new CreateAction(arType, prefix);
        controller.DynamicActions["edit"] = new EditAction(arType, prefix);
        controller.DynamicActions["update"] = new UpdateAction(arType, prefix);
        controller.DynamicActions["confirmdelete"] = new ConfirmDeleteAction(arType, prefix);
        controller.DynamicActions["delete"] = new DeleteAction(arType, prefix);
    }
}

As you see it doesn’t do much… just grab the attribute and registers dynamic actions on the controller.

Each dynamic action vary in complexity. It has to deal with API that users usually don’t see. But as you can see it’s not rocket science.

The ListAction:

public class ListAction : IDynamicAction
{
    private readonly Type arType;

    public ListAction(Type arType)
    {
        this.arType = arType;
    }

    public void Execute(Controller controller)
    {
        controller.PropertyBag["items"] = ActiveRecordMediator.FindAll(arType);

        controller.RenderView("list");
    }
}

The New/Create pair of actions:

public class NewAction : IDynamicAction
{
    public void Execute(Controller controller)
    {
        controller.RenderView("new");
    }
}

public class CreateAction : IDynamicAction
{
    private readonly Type arType;
    private readonly string prefix;

    public CreateAction(Type arType, String prefix)
    {
        this.arType = arType;
        this.prefix = prefix;
    }

    public void Execute(Controller controller)
    {
        object instance = null;
        
        try
        {
            ARSmartDispatcherController arController =
                (ARSmartDispatcherController) controller;

            ARDataBinder binder = (ARDataBinder) arController.Binder;
            binder.AutoLoad = AutoLoadBehavior.OnlyNested;

            TreeBuilder builder = new TreeBuilder();

            instance = binder.BindObject(
                arType, prefix,
                builder.BuildSourceNode(controller.Form));

            ActiveRecordMediator.Create(instance);
            
            controller.Redirect(controller.Name, "list");
        }
        catch(Exception ex)
        {
            controller.Flash["errormessage"] = ex.Message;
            controller.Flash[prefix] = instance;

            controller.Redirect(controller.Name, "new");
        }
    }
}

The Edit/Update pair of actions:

public class EditAction : IDynamicAction
{
    private readonly Type arType;
    private readonly string prefix;

    public EditAction(Type arType, String prefix)
    {
        this.arType = arType;
        this.prefix = prefix;
    }

    public void Execute(Controller controller)
    {
        int id = Convert.ToInt32(controller.Query["id"]);

        controller.PropertyBag[prefix] = 
            ActiveRecordMediator.FindByPrimaryKey(arType, id);
        
        controller.RenderView("edit");
    }
}

public class UpdateAction : IDynamicAction
{
    private readonly Type arType;
    private readonly string prefix;

    public UpdateAction(Type arType, String prefix)
    {
        this.arType = arType;
        this.prefix = prefix;
    }

    public void Execute(Controller controller)
    {
        object instance = null;

        try
        {
            ARSmartDispatcherController arController =
                (ARSmartDispatcherController) controller;

            ARDataBinder binder = (ARDataBinder) arController.Binder;
            binder.AutoLoad = AutoLoadBehavior.Always;
            
            TreeBuilder builder = new TreeBuilder();

            instance = binder.BindObject(
                arType, prefix,
                builder.BuildSourceNode(controller.Form));

            ActiveRecordMediator.Update(instance);

            controller.Redirect(controller.Name, "list");
        }
        catch (Exception ex)
        {
            controller.Flash["errormessage"] = ex.Message;
            controller.Flash[prefix] = instance;

            controller.Redirect(controller.Name, "edit", controller.Query);
        }
    }
}

The Confirm/Delete pair of actions:

public class ConfirmDeleteAction : IDynamicAction
{
    private readonly Type arType;
    private readonly string prefix;

    public ConfirmDeleteAction(Type arType, String prefix)
    {
        this.arType = arType;
        this.prefix = prefix;
    }

    public void Execute(Controller controller)
    {
        int id = Convert.ToInt32(controller.Query["id"]);

        controller.PropertyBag[prefix] = 
            ActiveRecordMediator.FindByPrimaryKey(arType, id);

        controller.RenderView("confirmdelete");
    }
}

public class DeleteAction : IDynamicAction
{
    private readonly Type arType;
    private readonly string prefix;

    public DeleteAction(Type arType, String prefix)
    {
        this.arType = arType;
        this.prefix = prefix;
    }

    public void Execute(Controller controller)
    {
        object instance = null;

        try
        {
            int id = Convert.ToInt32(controller.Form[prefix + ".id"]);
            
            instance = ActiveRecordMediator.FindByPrimaryKey(arType, id);

            ActiveRecordMediator.Delete(instance);

            controller.Redirect(controller.Name, "list");
        }
        catch (Exception ex)
        {
            controller.Flash["errormessage"] = ex.Message;
            controller.Flash[prefix] = instance;

            controller.Redirect(controller.Name, "confirmdelete", controller.Query);
        }
    }
}

Feel free to download this sample. You can use dynamic actions to create web application that supports plugins or are extensible in some related way. MonoRail also has a controller tree that allows new controllers to be registered in runtime, but I will talk about it some other day.

Categories: Castle | Top Of Page | 12 Comments » |

12 Responses to “MonoRail: Using DynamicActions and DynamicActionProviders”

Ernst Naezer Says:

Hammett, thanks this is really usefull!

Drew Burlingame Says:

Hammett,

this is awesome. It elegantly addresses the design principal: Favor composition over inheritance. I’ve just implemented it in my code, and instead of using the CrudAttribute, I did: CrudActionProvider : IDynamicActionProvider where T: ActiveRecordBase, new(). And all actions extend an AbstractCrudAction. within AbstractCrudAction, I get the type and prefix statically, so only once per type. I’d be happy to post if you’d like.

Drew Burlingame Says:

correction: CrudActionProvider should read CrudActionProvider

Drew Burlingame Says:

I’ll try again: CrudActionProvider

Drew Burlingame Says:

can’t get the generic label to take. it’s reading it as html. feel free to delete all these corrections.

hammett Says:

Drew, some people are preparing a site for useful snippets related to castle. I hope they dont give up. And I hope you contribute your code on it.

joeyDotNet Says:

Whoah. I just realized how awesome this is. I briefly looked at the dynamic actions stuff in the docs, but didn’t really have a chance to dive deeper into it. Man, this is going to solve a problem that’s been bothering me in my current MonoRail project, in a very cool way.

Thanks!

Zen and the art of Castle maintenance » Archives » Extending and customizing MonoRail Says:

[...] By default actions are instance methods on your controller classes. You can also use dynamic actions. But another approach is to use a default action. [...]

Sean Chambers Says:

This is a great find. I was struggling on how to make use of a common action to upload a file to the server, and then return an object that encapsulates the file for using elsewhere in my domain model.

I was trying it with view components, which solves the view aspect but got into some trouble when I tried implementing the controller portion of the component.

thanks! let us know where the snippet site is and I can post the code I used to accomplish this.

MonoRail: Dynamic Actions; The better way to implement actions? « Adam Esterline Says:

[...] would we want to use Dynamic Actions? I will let Hammett answer this [...]

drozzy Says:

A little bug, on Line 53 of the CategoryController:

category.Create()

should be:

category.Update()

Danyal Says:

I’m wondering how an overloaded action should be implemented using dynamic actions, e.g. handling all three of these with the same action:

/products/list.rails
/products/list.rails?page=2
/products/list.rails?page=2&filter=something

Should I create separate dynamic actions for each overload, or should I just handle the various querystring possibilities within a single dynamic action?

Thanks,
Danny

Leave a Reply