+1 vote

I have several concerns with how the custom providers works https://www.limilabs.com/static/mail/documentation/html/T_Limilabs_Mail_Templates_ITemplateDataProvider.htm

I have list of history items:

public class HistoryItem {
    public string FieldName {get;set;}
    public string OldValue {get;set;}
    public string NewValue {get;set;}    
}

FieldName is basically name of some property like Title or StartDate. I would like to provide all history items as the data source for the email template and access them via the FieldName as a key:

[if Title]
  Old Title: <b>[Title.Old]</b>, New Title: <b>[Title.New]</b> <br/>
[end]

[if StartDate]
  Old StartDate: <b>[StartDate.Old]</b>, New StartDate: <b>[StartDate.New]</b> <br/>
[end]

[if EndDate]
  Old EndDate: <b>[EndDate.Old]</b>, New EndDate: <b>[EndDate.New]</b> <br/>
[end]

I render template with the Custom Provider:

IList<HistoryItem> changes
Template temp = Template.Create(template).DataFrom(new CustomProvider(changes));

I have created custom provider as something like that:

internal class CustomProvider : ITemplateDataProvider
{
    private IList<HistoryItem> changes;

    public CustomProvider(IList<HistoryItem> changes)
    {
        this.changes = changes;
    }

    public object GetValue(string key)
    {
        var fieldName = key;

        if (fieldName.Contains("."))
        {
            var strings = fieldName.Split('.');
            fieldName = strings[0];
            change = changes.SingleOrDefault(x => x.FieldName == fieldName);

            switch (strings[1].ToLower())
            {
                case "old":
                    return change.NewValue;
                case "new":
                    return change.OldValue;
            }
        } 

        return key;
    }

    public bool HasValue(string name)
    {
        var fieldName = name;

        if (fieldName.Contains("."))
        {
            var strings = fieldName.Split('.');
            fieldName = strings[0];
        }
        return changes.Any(x => x.FieldName == fieldName);
    }
} 

The catch is that changes list does not always have all the properties referenced in the template. So sometimes it's going to be one element with FieldName Title, or sometimes two elements with FieldNames Title and EndDate and so on.

First problem is with the HasValue method in the provider. When it returns false whole thing just throws an exception. For example if changes does not contain the StartDate item, HasValue return false and then I get exception:

'StartDate' was not found.

So the solution is to always return true from this method (what is the purpose of this method then?)

    public bool HasValue(string name)
    {
        return true;
    }

But in this case there is a problem with [if] statements in the template. Basically there is no distinction between using [if Title] and [Title]. Both of them will fall under GetValue method with "Title" as a key attribute, but one of them requires to return boolean and other one string. For example if I use [if Title] in the template and GetValue method will return string - I'm going to get invalid cast exception.

I imagine the solution would be as if the HasValue returns false - then do not call the GetValue method (instead of throwing exception).

What am I missing here?

by (260 points)
edited by

1 Answer

+1 vote

what is the purpose of this method then?

As usually there are several ITemplateDataProviders, HasValue method is used to determine if this specific one can return a value.

[if Title]

[if XXX] template element must reference boolean property or rather boolean must be returned from GetValue method.

I imagine the solution would be as if the HasValue returns false - then do not call the GetValue method (instead of throwing exception).

GetValue is not called on this provider, but if all providers return false. Exception is thrown.

Example:

internal class CustomProvider2 : ITemplateDataProvider
{
    public object GetValue(string key)
    {
        if (key == "IsKey1")
            return false;
        if (key == "IsKey2")
            return true;

        if (key == "Key1")
            throw new Exception("Won't be asked for, as IsKey1 returns false");
        if (key == "Key2")
            return "Value2";
        return null;
    }

    public bool HasValue(string key)
    {
        return true;
    }
}

Unit test:

var t = Template.Create("[if IsKey1][Key1][end][if IsKey2][Key2][end]");
Assert.AreEqual("Value2", template.DataFrom(new CustomProvider2()).Render());
by (301k points)
...