Creating custom tokens for a custom part

Topics: Writing modules
Jul 25, 2012 at 12:47 PM
Edited Jul 25, 2012 at 12:53 PM

Background:

I've developed a custom module for managing guests to an event, this includes:

  • Several models and content parts
  • Drivers and handlers for the content parts
  • Custom admin section for managing guests (status, details etc)
  • Views for CRUD-operations (site and admin)

I now would like to implement some functionality for sending emails to a guest after registration, this email should contain all the details the guest entered and some information that the module automatically adds. Ideally the email would grab a details view of the content type as its body.

I've tried Custom Forms but I wanted more fine grained control over everything plus I wanted to learn Orchard. So switching to this module is not an option for me.

What I would like to do:

I've tried to implement custom tokens to cleanly separate concerns, whenever a content type of 'Guest' is created I would like to send an email to the guests email address containing all the details they've entered. To do this I've created a rule (when content with types 'Guest' is created...), The problem is that I never get any values for my tokens.

The following code snippets shows the relevant details of my implementation:

 

// content part
public class GuestPart : ContentPart<GuestPartRecord> {
    [Required]
    [StringLength(50)]
    public string FirstName {
        get { return Record.FirstName; }
        set { Record.FirstName = value; }
    }
    [Required]
    [StringLength(50)]
    public string LastName {
        get { return Record.LastName; }
        set { Record.LastName = value; }
    }
    // etc, etc
}
// handler
public class GuestPartHandler : ContentHandler {
    public GuestPartHandler(IRepository<GuestPartRecord> repository) {
        Filters.Add(StorageFilter.For(repository));
    }
}
// driver method for creating records
protected override DriverResult Editor(GuestPart part, IUpdateModel updater, dynamic shapeHelper) {
    var model = new EditGuestViewModel();
    updater.TryUpdateModel(model, Prefix, null, null);

    if (part.ContentItem.Id != 0) {
        _guestService.UpdateGuestForContentItem(part.ContentItem, model);
    }

    return Editor(part, shapeHelper);
}
// token code, implements ITokenProvider that inherits IEventHandler
public void Describe(dynamic context) {
    context.For("Guest", T("Guest Items"), T("Tokens for Guest"))
        .Token("FirstName", T("Guest First Name"), T("The first name of a guest."))
        .Token("LastName", T("Guest Last Name"), T("The last name of a guest."))
        ;
}
public void Evaluate(dynamic context) {
    context.For<IContent>("Content")
        .Token("FirstName", (Func<IContent, object>)(content => content.As<GuestPart>().FirstName))
        .Token("LastName", (Func<IContent, object>)(content => content.As<GuestPart>().LastName))
        ;
}

 

I've read Bertrand's blog about tokens but to be honest I still don't know how to fetch the current content item being created using the Evaluate-method. It's simple if I wanted to do the MadLib-implementation (just a new instance of a class) but that's more of an example than a real world implementation.

I've googled for hours searching for more examples and I've only found a russian page that shows some implementation (couldn't get that to work either). I've looked in detail at the following:

  • CommentTokens (from the Comment module)
  • ContentTokens, DateTokens, RequestTokens, TextTokens, UserTokens
  • ProductTokens (from Nwazet.Commerce)

I've tried all permutations of this as I can think of but no luck... I can see my code getting executed (two times per request) but I never get any values from my content part - other tokens are working perfectly.

Does anybody have some examples of a working solution or any pointers about my code?

Any help is greatly appreciated! :-)

Coordinator
Jul 26, 2012 at 2:16 PM

What are you seeing executed? the method or the Lambdas in the tokens?

Jul 26, 2012 at 8:38 PM

Both the method and the Lambdas but they never yield any results. I think that the actual content record is missing from the context, when I've stepped through the code it seems like my content part record isn't available. Should it be available or do I have to add some code for this?

When I look at your code in Nwazet.Commerce I can't see how your Lambdas yield any results unless I've missed something else?

Coordinator
Jul 27, 2012 at 1:39 PM

What type are you extending? From what you're saying, your parts are not on the items at the time when the tokens get evaluated?

Aug 1, 2012 at 12:23 AM

Been away for a couple of days but took another look at it today. My content type is called 'Guest' and it includes the 'Common' and 'Guest' parts.

One major differences between my code and yours is that I add my viewmodel and not the raw part in my driver methods. The code above is one try but I've also tried the following:

protected override DriverResult Editor(GuestPart part, dynamic shapeHelper) {
    return ContentShape("Parts_Guest_Edit",
        () => shapeHelper.EditorTemplate(
            TemplateName: TemplateName,
            Model: BuildEditorViewModel(part),
            Prefix: Prefix));
}

protected override DriverResult Editor(GuestPart part, IUpdateModel updater, dynamic shapeHelper) {
    var model = BuildEditorViewModel(part);
    updater.TryUpdateModel(model, Prefix, null, null);

    if (part.ContentItem.Id != 0) {
        _guestService.UpdateGuestForContentItem(part.ContentItem, model);
    }

    return Editor(part, shapeHelper);
}

I actually enabled Nwazet.Commerce today and tried to add a rule that displayed the notification "Product with SKU {Content.SKU} created..." and it didn't yield a correct result. The SKU token is actually empty, which yields a notification without any SKU value, but I can see that it has been added to the context. I also found your ProductPart with data when I debugged - this differs from my implementation in that I can see that my custom part is available in the context but it never holds any data. But the actual content records for my custom content type are always created correct - I just can't seem to get tokens to work.

If you have any insights it would be great, I have to start implementing the email functionality without tokens because my deadline for the project is closing fast.

Coordinator
Aug 2, 2012 at 4:31 PM

I'm asking around. It seems like there is a timing issue here.

Mar 26, 2013 at 6:21 PM
Any update on this?

I have exactly the same problem.
Coordinator
Mar 27, 2013 at 8:19 AM
Do you have a reliable repro?
Mar 27, 2013 at 11:52 AM
What do you mean? Do you want access to the repository with the code for this?
Developer
Mar 27, 2013 at 10:47 PM
I think Bertrand means if you could provide steps to reproduce this issue with a clean instance of Orchard and perhaps a stripped down version of your module that we can use to reproduce the issue you're seeing.
Coordinator
Mar 27, 2013 at 11:13 PM
Exactly.
Apr 8, 2013 at 10:02 AM
I did actually get mine working, heres my code for future reference for anyone..
public void Describe(DescribeContext context) {
    context.For("Content", T("Content"), T("Tokens for course"))
        .Token("Course.Parent-Sector", T("Course.Parent-Sector"), T("The parent sector for the course"));
}

public void Evaluate(EvaluateContext context) {
    context.For<IContent>("Content")
        .Token("Course.Parent-Sector", (GetParentTermSlug));
}

public string GetParentTermSlug(IContent content)
{
   ... do something with content, return a string
}
May 28, 2014 at 9:37 PM
Old post, but I thought I would leave my comments for anyone having trouble in the future...

I was having the same issue, trying to define tokens on a custom part, and make the tokens available under a custom target (I wanted {MyPart.Something} rather than {Content.Something} or {Content.MyPart.Something}). Not sure if this is relevant, but I wanted to make use of this token within a workflow.

Code similar to this kept failing (Evaluate was hit, but evaluateToken either wasn't hit, or didn't produce a result)
public void Describe(DescribeContext context)
{
    context.For("MyPart", T("MyPart"), T("Tokens for my part."))
             .Token("CustomToken", T("CustomToken"), T("A custom token."));
}

public void Evaluate(EvaluateContext context)
{
    context.For<IContent>("MyPart")
             .Token("CustomToken", evaluateToken);
}

private string evaluateToken(IContent contentItem)
{
    //some operation involving contentItem, returning a property value (string)
}
As the OP commented, it seemed my problem was the current content item (the one with my part attached to it) was not getting passed along as data. Using the MadLibs approach of just passing a new MyPart() for default data to the For method wasn't going to work - I needed the current instance of MyPart

Getting the current content item within Evaluate is possible, at least in my scenario. Here's how I ended up doing it
public void Describe(DescribeContext context)
{
    context.For("MyPart", T("MyPart"), T("Tokens for my part."))
             .Token("CustomToken", T("CustomToken"), T("A custom token."));
}

public void Evaluate(EvaluateContext context)
{
    //the "current" content item is available from the context's Data property (a Dict<string, object>)
    IContent currentContentItem = context.Data["Content"] as IContent;

    //now cast to get the current instance of MyPart
    MyPart currentMyPart = currentContentItem == null ? null : currentContentItem.As<MyPart>();

    //finally, provide the current instance of MyPart as default data to the For method
    //NOTICE: the generic type parameter of For is no longer IContent - I'm working with MyPart now
    context.For<MyPart>("MyPart", currentMyPart)
             .Token("CustomToken", evaluateToken);
}

//evaluateToken now works directly with the current instance of MyPart
private string evaluateToken(MyPart currentPart)
{
    //some operation involving currentPart, returning a property value (string)
}