Trying to create a new Action for the Rules Engine

Topics: Customizing Orchard, Writing modules
Nov 16, 2011 at 11:54 PM
Edited Nov 17, 2011 at 12:41 AM

I'm trying to create a new action for the rules engine to send my templated emails but I'm running into some trouble. Here's the code for my action.

   public interface IActionProvider : IEventHandler
    {
        void Describe(dynamic describe);
    }
 
    public class SendTemplatedEmailAction : IActionProvider
    {
        private ITemplatedEmailSendingService _templatedEmailSendingService;
        private IContentManager _contentManager;
        private IMembershipService _membershipService;
        public const string MessageType = "SendTemplatedEmail";
 
 
        public Localizer T { getset; }
 
        public SendTemplatedEmailAction(ITemplatedEmailSendingService templatedEmailSendingService, IContentManager contentManager,IMembershipService membershipService)
        {
            _templatedEmailSendingService = templatedEmailSendingService;
            _contentManager = contentManager;
            _membershipService = membershipService;
        }
 
        public void Describe(dynamic describe) {
            Func<dynamicLocalizedString> display = context => T("Send a templated e-mail");
 
            describe.For("Messaging", T("Messaging"), T("Messages"))
                .Element("ActionTemplatedEmail", T("Send Templated Email"), T("Sends an e-mail to a specific user."), (Func<dynamicbool>)Send, display, "ActionTemplatedEmail");
        }
 
        private bool Send(dynamic context) {
            var user = _membershipService.GetUser("admin");
 
            _templatedEmailSendingService.SendTemplatedEmailToUser(user, "Test");
 
            return true;
        }
    }

All seemed fine and the action showed up in the list but when I clicked on it to actually try and add it what I got was this.

 

Server Error in '/' Application.

Sequence contains no matching element

Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Exception Details: System.InvalidOperationException: Sequence contains no matching element

Source Error:

 

Line 146:            public void EndProcessRequest(IAsyncResult result) {
Line 147:                try {
Line 148:                    _httpAsyncHandler.EndProcessRequest(result);
Line 149:                }
Line 150:                finally {


Source File: C:\Work\PurchasEdge\Phoenix.Dev\Orchard\src\Orchard\Mvc\Routes\ShellRoute.cs    Line: 148

Stack Trace:

[InvalidOperationException: Sequence contains no matching element]
   System.Linq.Enumerable.First(IEnumerable`1 source, Func`2 predicate) +338
   Orchard.Forms.Services.DefaultFormManager.Build(String formName, String prefix) +437
   Orchard.Rules.Controllers.ActionController.Edit(Int32 id, String category, String type, Int32 actionId) +737
   lambda_method(Closure , ControllerBase , Object[] ) +262
   System.Web.Mvc.ReflectedActionDescriptor.Execute(ControllerContext controllerContext, IDictionary`2 parameters) +264
   System.Web.Mvc.ControllerActionInvoker.InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary`2 parameters) +39
   System.Web.Mvc.<>c__DisplayClass15.<InvokeActionMethodWithFilters>b__12() +129
   System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodFilter(IActionFilter filter, ActionExecutingContext preContext, Func`1 continuation) +826410
   System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodFilter(IActionFilter filter, ActionExecutingContext preContext, Func`1 continuation) +826410
   System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodFilter(IActionFilter filter, ActionExecutingContext preContext, Func`1 continuation) +826410
   System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodWithFilters(ControllerContext controllerContext, IList`1 filters, ActionDescriptor actionDescriptor, IDictionary`2 parameters) +314
   System.Web.Mvc.ControllerActionInvoker.InvokeAction(ControllerContext controllerContext, String actionName) +825632
   System.Web.Mvc.Controller.ExecuteCore() +159
   System.Web.Mvc.ControllerBase.Execute(RequestContext requestContext) +335
   System.Web.Mvc.<>c__DisplayClassb.<BeginProcessRequest>b__5() +62
   System.Web.Mvc.Async.<>c__DisplayClass1.<MakeVoidDelegate>b__0() +20
   System.Web.Mvc.<>c__DisplayClasse.<EndProcessRequest>b__d() +54
   Orchard.Mvc.Routes.HttpAsyncHandler.EndProcessRequest(IAsyncResult result) in C:\Work\PurchasEdge\Phoenix.Dev\Orchard\src\Orchard\Mvc\Routes\ShellRoute.cs:148
   System.Web.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +469
   System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +375



Version Information: Microsoft .NET Framework Version:4.0.30319; ASP.NET Version:4.0.30319.237

 

So I tried adding this which was from the email action in the email module.

    public interface IFormProvider : IEventHandler
    {
        void Describe(dynamic context);
    }
 
    public class SendTemplatedEmailForm : IFormProvider
    {
        protected dynamic Shape { getset; }
        public Localizer T { getset; }
 
        public SendTemplatedEmailForm(IShapeFactory shapeFactory) {
            Shape = shapeFactory;
        }
        public void Describe(dynamic context)
        {
            Func<IShapeFactorydynamic> form =
                shape => Shape.Form(
                Id: "ActionTemplatedEmail",
                _Type: Shape.SelectList(
                    Id: "Recipient", Name: "Recipient",
                    Title: T("Send to"),
                    Description: T("Select who should be the recipient of this e-mail."))
                    .Add(new SelectListItem { Value = "owner", Text = T("Owner").Text })
                    .Add(new SelectListItem { Value = "author", Text = T("Author").Text })
                    .Add(new SelectListItem { Value = "admin", Text = T("Site Admin").Text }),
                _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("ActionTemplatedEmail", form);
        }
    }

But I still get the same error. Any ideas what I'm missing?

 

Thanks much

EDIT: Code updated.

Coordinator
Nov 17, 2011 at 12:03 AM

I have to ask: why don't you use the one that comes with email rules?

Nov 17, 2011 at 12:06 AM

Primarily because

1. We want to be able to customize the from address.

2. We want to be able to do token replacement in the emails.

 

Also I think having the emails as a content part gives us more flexibility.

Coordinator
Nov 17, 2011 at 12:08 AM

The existing one already does token replacement. You should be able to start from that code, at least.

Nov 17, 2011 at 12:15 AM

I was starting from that code. The IActionProvider is a copy of that code with just a few changes and a changed send method to use the service I made. The IFormProvider is nearly a direct copy I just changed some strings so that they matched up with what looked like they should from the IActionProvider.

Coordinator
Nov 17, 2011 at 12:31 AM

Your Action is described as using the "ActionEmail" form, whereas the form you declare in named "SendTemplatedEmail".

Nov 17, 2011 at 12:42 AM

Thanks. I noticed that and changed it but I still get the same error. I updated the OP with my current code.

Nov 17, 2011 at 1:02 PM

Does the normal Email action still work, or cause the same error?

Also, I'm interested in your implementation of ITemplatedEmailSendingService - templated emails are something I'd like to get working myself.

Nov 17, 2011 at 3:53 PM

I notice the constructor is missing:

T = NullLocalizer.Instance;

(And this is missing in Email Rules also)

Usually this won't matter since the dependency gets injected in a different way; but if for any reason there wasn't a proper localizer present this could cause an error?

Nov 17, 2011 at 5:19 PM
Edited Nov 17, 2011 at 5:45 PM

I just copied the code from the email action in the email module. The mail action in the Orchard module is also missing the T = NullLocalizer.Instance;

I tried putting that in the constructor but it does not change the error message. My I TemplatedEmailSendingService is actually quite simple it just uses token replacements.However I'm not sure that I'm using content Items correctly and I haven't yet tested that it actually works. I was planning on testing it with the action as I thought the action would be really simple and easy to get working but here is the code anyway for your viewing pleasure.

 

    public interface  ITemplatedEmailSendingService : IDependency {
        void SendTemplatedEmailToUser(IUser to, string templatedEmailName);
    }
 
    public class TemplatedEmailSendingService : ITemplatedEmailSendingService
    {
        private IRepository<EmailablePart> _emailTemplates;
        private IMessagingChannel _emailChannel;
        private ITokenizer _tokenizer;
        private IContentManager _contentManager;
 
        public TemplatedEmailSendingService(IMessagingChannel emailChannel, ITokenizer tokenizer, IContentManager contentManager)
        {
            _tokenizer = tokenizer;
            _emailChannel = emailChannel;
            _contentManager = contentManager;
        }
 
        public void SendTemplatedEmailToUser(IUser to, string templatedEmailName) {
            var templatedEmail = _contentManager.Query().ForType(new[] {"TemplatedEmail"}).List().Where(x => x.As<EmailablePart>().Name == templatedEmailName).FirstOrDefault();
 
            var message = new MessageContext();
            message.Recipient = to.ContentItem.Record;
            var emailablePart = templatedEmail.As<EmailablePart>();
            message.MailMessage.Subject = _tokenizer.Replace(emailablePart.Subject, null);
            message.MailMessage.From = new MailAddress(emailablePart.FromAddress);
            message.MailMessage.Body = _tokenizer.Replace(templatedEmail.As<BodyPart>().Text, null);
 
 
            _emailChannel.SendMessage(message);
        }
    }

EDIT: In debugging the code it looks like as I suspected the IFormProvider I made was not getting added to the list of form providers.
Nov 17, 2011 at 5:44 PM

I've managed to reproduce the same error you're seeing, in my Audit module.

What I was doing was adding a new "Content" category event for when content is viewed.

I get this exception when I attempt to add my new event to a rule.

I wondered if it was because the category was named the same as an existing category, or that the classes were named the same as those in contents, but I've tried renaming everything and still get the error.

It's very weird! I have successfully added new Actions though, for creating Audit records, and it all works perfectly (you can see this at http://orchardaudit.codeplex.com/)

So I'm a bit baffled at this stage but I'll keep trying things out...

My exception was as follows:

Sequence contains no matching element

Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code. 

Exception Details: System.InvalidOperationException: Sequence contains no matching element

Source Error: 

Line 146:            public void EndProcessRequest(IAsyncResult result) {
Line 147:                try {
Line 148:                    _httpAsyncHandler.EndProcessRequest(result);
Line 149:                }
Line 150:                finally {


Source File: G:\Orchard\Default\src\Orchard\Mvc\Routes\ShellRoute.cs    Line: 148 

Stack Trace: 

[InvalidOperationException: Sequence contains no matching element]
   System.Linq.Enumerable.First(IEnumerable`1 source, Func`2 predicate) +278
   Orchard.Forms.Services.DefaultFormManager.Build(String formName, String prefix) +368
   Orchard.Rules.Controllers.EventController.Edit(Int32 id, String category, String type, Int32 eventId) +1086
   lambda_method(Closure , ControllerBase , Object[] ) +259
   System.Web.Mvc.ActionMethodDispatcher.Execute(ControllerBase controller, Object[] parameters) +17
   System.Web.Mvc.ReflectedActionDescriptor.Execute(ControllerContext controllerContext, IDictionary`2 parameters) +208
   System.Web.Mvc.ControllerActionInvoker.InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary`2 parameters) +27
   System.Web.Mvc.<>c__DisplayClass15.<InvokeActionMethodWithFilters>b__12() +55
   System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodFilter(IActionFilter filter, ActionExecutingContext preContext, Func`1 continuation) +263
   System.Web.Mvc.<>c__DisplayClass17.<InvokeActionMethodWithFilters>b__14() +19
   System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodFilter(IActionFilter filter, ActionExecutingContext preContext, Func`1 continuation) +263
   System.Web.Mvc.<>c__DisplayClass17.<InvokeActionMethodWithFilters>b__14() +19
   System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodFilter(IActionFilter filter, ActionExecutingContext preContext, Func`1 continuation) +263
   System.Web.Mvc.<>c__DisplayClass17.<InvokeActionMethodWithFilters>b__14() +19
   System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodWithFilters(ControllerContext controllerContext, IList`1 filters, ActionDescriptor actionDescriptor, IDictionary`2 parameters) +191
   System.Web.Mvc.ControllerActionInvoker.InvokeAction(ControllerContext controllerContext, String actionName) +343
   System.Web.Mvc.Controller.ExecuteCore() +116
   System.Web.Mvc.ControllerBase.Execute(RequestContext requestContext) +97
   System.Web.Mvc.ControllerBase.System.Web.Mvc.IController.Execute(RequestContext requestContext) +10
   System.Web.Mvc.<>c__DisplayClassb.<BeginProcessRequest>b__5() +37
   System.Web.Mvc.Async.<>c__DisplayClass1.<MakeVoidDelegate>b__0() +21
   System.Web.Mvc.Async.<>c__DisplayClass8`1.<BeginSynchronous>b__7(IAsyncResult _) +12
   System.Web.Mvc.Async.WrappedAsyncResult`1.End() +62
   System.Web.Mvc.<>c__DisplayClasse.<EndProcessRequest>b__d() +50
   System.Web.Mvc.SecurityUtil.<GetCallInAppTrustThunk>b__0(Action f) +7
   System.Web.Mvc.SecurityUtil.ProcessInApplicationTrust(Action action) +22
   System.Web.Mvc.MvcHandler.EndProcessRequest(IAsyncResult asyncResult) +60
   System.Web.Mvc.MvcHandler.System.Web.IHttpAsyncHandler.EndProcessRequest(IAsyncResult result) +9
   Orchard.Mvc.Routes.HttpAsyncHandler.EndProcessRequest(IAsyncResult result) in G:\Orchard\Default\src\Orchard\Mvc\Routes\ShellRoute.cs:148
   System.Web.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +8862381
   System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +184

 

Coordinator
Nov 17, 2011 at 5:45 PM

The current email action implementation is also "templated" using tokens. The only thing I think is missing from the Email Rule is the ability to provide a specific Username, or email. Right now you can use Site Admin, Author and Owner.

Coordinator
Nov 17, 2011 at 5:46 PM

Debugging inside the DefaultFormManager.Build method should give the answer.

Nov 17, 2011 at 5:56 PM

Right ... in my case the error was pretty obvious. I had a typo in the Id of the form I was calling. There's a bug in the way the DefaultFormManager looks for the form by name:

            var descriptor = context.Describe().First(x => x.Name == formName);

            if (descriptor == null) {
                return null;
            }

I hope the error in that code is obvious - First will never return null, it'll throw an exception if the name isn't found. So the null check that follows it is pointless. The exception isn't handled and we get a confusing error message.

In your case, from the code you pasted, the form Id does match so I have no idea why you're seeing the error, although can you go back and double check to make sure you haven't accidentally changed a character somewhere?

 

Nov 17, 2011 at 6:00 PM
sebastienros wrote:

The current email action implementation is also "templated" using tokens. The only thing I think is missing from the Email Rule is the ability to provide a specific Username, or email. Right now you can use Site Admin, Author and Owner.

By "templated" I assumed sarudak wishes to use a HTML template for the email (you can see in his code he's accessing a BodyPart from a content item).

Actually the way I want to do templating is slightly different, I want to render an email using .cshtml files; I don't want to be pasting a load of HTML code into a form field in Rules. I want to have a themed template which wraps around the email body coming from Rules (and also use that template for anything that sends an email, not just Rules). But that's a separate project, anyway. 

Nov 17, 2011 at 6:14 PM

The funny thing is when I debug the default form manager there is only one item in the _formProviders collection.  And though the debugger is completely unhelpful in telling me what that one item is when the describe gets called it goes to NotificationForms this would indicate that the IFormsProvider is not being recognized as a FormsProvider for some reason?

Nov 17, 2011 at 6:30 PM
Edited Nov 17, 2011 at 6:30 PM

What namespace is your IFormProvider in? I'm wondering if you copy/pasted the code but forgot to change the namespace to your own. Having the same interface in the same namespace twice could certainly confuse the IoC.

Nov 17, 2011 at 6:35 PM

No the namespace is mine. Also the orchard email module isn't even enabled.

Nov 17, 2011 at 7:07 PM

Figured out the problem. There was apparently a stale .dll in my bin folder and for some reason orchard was not recompiling it even though the source code had changed. I deleted it and restarted orchard and everything worked.

Nov 17, 2011 at 8:00 PM

It would seem to be a simple fix to the from address problem but could this line

context.MailMessage.From = new MailAddress(smtpSettings.Address);

in the EmailMessageChannel.cs be changed to

   if (context.MailMessage.From == null)
       context.MailMessage.From = new MailAddress(smtpSettings.Address);

Would that be something I would submit a patch for? This would seem an obvious and simple solution that would keep the SMTP settings from address as a default but would allow it to be overridden.

Coordinator
Nov 17, 2011 at 8:06 PM

Sure, a patch would be great. Thanks.

Nov 22, 2011 at 1:01 AM

So I'm now trying to create an event to go along with my action and for some reason I assumed that on my action I could call for a piece of information like so

var user = (IUser) context.Properties["EmailRecipient"];

if said piece of information was passed into my event when it was triggered like so.

_rulesManager.TriggerEvent("Sample""Sample", () => new Dictionary<stringobject> { { "EmailRecipient", _workContextAccessor.GetContext().CurrentUser } });

But it's not working for me. Am I supposed to be able to transfer information from events to actions this way? If not then what is the story around transfer of information from events to actions?

Coordinator
Nov 22, 2011 at 1:53 AM

Why not take advantage of model binding and build an interface with a method that takes an EmailRecipient parameter?

Nov 22, 2011 at 6:20 AM

I think it should work, you just need to access context.Tokens instead of Properties.

Nov 22, 2011 at 7:13 PM

Thanks! Using Tokens worked great!