Custom action in Rules module

Topics: Customizing Orchard, Writing modules
May 30, 2013 at 10:33 AM
Hello
I'm new to Orchard and been trying to figure out this, for a couple of days.

I have a custom form build with CustomForm module. On that custom form I've set up a rule that sends an email when it's submitted. I would like to add automatically generated pdf to this email. For pdf creation i would use one of available open source libraries.

I want to know witch interface do i have to implement so that my custom action will be shown in "add an action" part of Rules module. I was looking at the Messaging module and couldn't find anything useful.

I was thinking of creating custom module that would add this custom action to rules and generate pdf and send an email manually. Is this even the right way to accomplish this kind of work flow.

Regards, Primož
Developer
May 30, 2013 at 10:41 AM
To create your own action, you need to implement Orchard.Rules.Services.IActionProvider (or, as some people do, create that interface in your own module if you don't want to add a reference to Orchard.Rules).
Developer
May 30, 2013 at 10:42 AM
What may be interesting to know is that 1.7 will have a Workflow module, where you could for example create an activity that generates a PDF, and another activity that sends an email, taking input from the previous activity as an attachment.
May 30, 2013 at 2:22 PM
So i added a new Class to Rules folder in my module.
namespace PDF.Rules
{
    public interface IActionProvider : IEventHandler
    {
        void Describe(dynamic describe);
    }
    public class PDFAction : IActionProvider
    {
        private IMessageManager _messageManager;
        private IOrchardServices _orchardServices;
        private IMembershipService _membershipService;
        public PDFAction(
            IMessageManager messageManager,
            IOrchardServices orchardServices,
            IMembershipService membershipService) {
            _messageManager = messageManager;
            _orchardServices = orchardServices;
            _membershipService = membershipService;
            T = NullLocalizer.Instance;
            Logger = NullLogger.Instance;
        }


        public void Describe(dynamic describe)
        {
            Func<dynamic, LocalizedString> display = context => T("Generate PDF");
            describe.For("Messaging", T("Messaging"), T("Messages"))
                .Element(
                    "GeneratePDF", T("Generate pdf"), T("Generate pdf and mybe send email"), (Func<dynamic, bool>)Generate,
                    display, "ActionGeneratePDF");
        }
        private bool Generate(dynamic arg)
        {
            Logger.Information("This is generate method");
            throw new NotImplementedException();
        }
        public Localizer T { get; set; }
        public ILogger Logger { get; set; }
    }
}
I can see this option in the Rules module. But when i click on it i get this error.
An unhandled exception has occurred and the request was terminated. Please refresh the page. If the error persists, go back

Cannot perform runtime binding on a null reference

Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: Cannot perform runtime binding on a null reference at CallSite.Target(Closure , CallSite , Object , Object ) at System.Dynamic.UpdateDelegates.UpdateAndExecuteVoid2[T0,T1](CallSite site, T0 arg0, T1 arg1) at Orchard.Rules.Controllers.ActionController.Edit(Int32 id, String category, String type, Int32 actionId) in c:\Users\Primoz\Downloads\Orchard.Source\src\Orchard.Web\Modules\Orchard.Rules\Controllers\ActionController.cs:line 85 at lambda_method(Closure , ControllerBase , Object[] ) at System.Web.Mvc.ActionMethodDispatcher.Execute(ControllerBase controller, Object[] parameters) at System.Web.Mvc.ReflectedActionDescriptor.Execute(ControllerContext controllerContext, IDictionary`2 parameters) at System.Web.Mvc.ControllerActionInvoker.InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary`2 parameters) at System.Web.Mvc.ControllerActionInvoker.<>c__DisplayClass13.<InvokeActionMethodWithFilters>b__10() at System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodFilter(IActionFilter filter, ActionExecutingContext preContext, Func`1 continuation) at System.Web.Mvc.ControllerActionInvoker.<>c__DisplayClass13.<>c__DisplayClass15.<InvokeActionMethodWithFilters>b__12() at System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodFilter(IActionFilter filter, ActionExecutingContext preContext, Func`1 continuation) at System.Web.Mvc.ControllerActionInvoker.<>c__DisplayClass13.<>c__DisplayClass15.<InvokeActionMethodWithFilters>b__12() at System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodFilter(IActionFilter filter, ActionExecutingContext preContext, Func`1 continuation)
I'm 100% sure that i have to do some more work in my module, but i cant find any tutorial regarding this topic. I would appreciate if you guys could help me and point me in the right direction on witch classes i have to add to my module.

Regards, Primož
Developer
May 30, 2013 at 5:12 PM
It looks like you specified the name of a form called "ActionGeneratePDF". Did you actually implement a form with that name using the Forms API?
May 30, 2013 at 5:25 PM
Ok so that is a form name :D. Tnx. Will implement a form and report lather.
May 30, 2013 at 5:44 PM
Its me again.
namespace PDF.Rules
{
    public interface IFormProvider : IEventHandler
    {
        void Describe(dynamic context);
    }
    public class PDFForm : IFormProvider
    {
        protected dynamic Shape { get; set; }
        public Localizer T { get; set; }

        public PDFForm(IShapeFactory shapeFactory)
        {
   
            Shape = shapeFactory;
            T = NullLocalizer.Instance;

        }
        public void Describe(dynamic context)
        {
            //this is just a partial copy form email module
            Func<IShapeFactory, dynamic> form =
             shape => Shape.Form(
             Id: "ActionGeneratePDF",
             _Subject: Shape.Textbox(
                 Id: "Subject", Name: "Subject",
                 Title: T("Subject"),
                 Description: T("The subject of the e-mail."),
                 Classes: new[] { "large", "text", "tokenized" }),
             _Message: Shape.Textarea(
                 Id: "Body", Name: "Body",
                 Title: T("Body"),
                 Description: T("The body of the e-mail."),
                 Classes: new[] { "tokenized" }
                 )
             );

            context.Form("ActionGeneratePDF", form);
        }
    }
}
This is the form i added to Rules folder. But now I'm getting this exception.
Cannot perform runtime binding on a null reference

Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: Cannot perform runtime binding on a null reference at CallSite.Target(Closure , CallSite , Object , Object ) at System.Dynamic.UpdateDelegates.UpdateAndExecuteVoid2[T0,T1](CallSite site, T0 arg0, T1 arg1) at Orchard.Rules.Controllers.ActionController.Edit(Int32 id, String category, String type, Int32 actionId) in c:\Users\Primoz\Downloads\Orchard.Source\src\Orchard.Web\Modules\Orchard.Rules\Controllers\ActionController.cs:line 85 at lambda_method(Closure , ControllerBase , Object[] ) at System.Web.Mvc.ActionMethodDispatcher.Execute(ControllerBase controller, Object[] parameters) at System.Web.Mvc.ReflectedActionDescriptor.Execute(ControllerContext controllerContext, IDictionary`2 parameters) at System.Web.Mvc.ControllerActionInvoker.InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary`2 parameters) at System.Web.Mvc.ControllerActionInvoker.<>c__DisplayClass13.<InvokeActionMethodWithFilters>b__10() at System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodFilter(IActionFilter filter, ActionExecutingContext preContext, Func`1 continuation) at System.Web.Mvc.ControllerActionInvoker.<>c__DisplayClass13.<>c__DisplayClass15.<InvokeActionMethodWithFilters>b__12() at System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodFilter(IActionFilter filter, ActionExecutingContext preContext, Func`1 continuation) at System.Web.Mvc.ControllerActionInvoker.<>c__DisplayClass13.<>c__DisplayClass15.<InvokeActionMethodWithFilters>b__12() at System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodFilter(IActionFilter filter, ActionExecutingContext preContext, Func`1 continuation)
What now:D do i need a controller or something similar.
btw. this looks relay painful for Orchard beginner.
Developer
May 30, 2013 at 6:00 PM
You don't need a controller. :)
Please attach the debugger and have it break on the thrown exception. Inspect variables, context and stack.
I know, it hurts like hell if you don't know what you're doing. But that will change and then you'll love it. Embrace the pain, it's good. ;)
May 31, 2013 at 6:36 AM
OK, so i decided that i don't need a form. And i will hardcore all the parameters for now :D. I have one question tho.
Where can i see what a certain parameter stand for? For instance what are the parameter for method "For" & "Element" in my Describe method.
        public void Describe(dynamic describe)
        {
            Func<dynamic, LocalizedString> display = context => T("Generate PDF");

            describe.For("Messaging", T("Messaging"), T("Messages"))
                .Element(
                    "GeneratePDF", T("Generate pdf"), T("Generate pdf and mybe send email"), (Func<dynamic, bool>)Generate,
                    display);
        }
btw: I removed last parm from Element method and i can set the action without form and get to the "Generate" method where i will generate the pdf and send it trough email.
Developer
May 31, 2013 at 12:53 PM
Edited May 31, 2013 at 7:19 PM
If you look at the "real" interface instead of your own version, you'll see the signature (look for IActionProvider in Orchard.Rules.Services). The real type used is DescribeActionContext. Perhaps it's easier to work with if you remove your own IActionProvider and reference Orchard.Rules from your module so you can work with the original IActionProvider interface. That way you'll have Intellisense to help you discover the API.
May 31, 2013 at 2:43 PM
Huh ok that was helpful :D
I managed to generate a pdf and send it with SmtpClient.

Just one more think (for now) :D

How would i get hold on the data that was entered in the form? Every parameter i get in is dynamic.
I was thinking. I would have to create a IFormProvider & then in the admin panel of Orchard set fields to "{Content.Fields.BuyForm.email}". Then i would have access to
var recipient = context.Properties["Recipient"];
Like in the Email modul? Or is there an easier way?
Developer
May 31, 2013 at 7:21 PM
Yes, look at the Orchard.Email module to see how it's done.