Thursday, 13 November 2014

Umbraco Mapper with Nested Archetypes

Following a previous blog post I got a comment asking about dealing with mapping nested Archetypes to a view model using Umbraco Mapper. As I noted in response, it's not that straightforward - the flexibility of the Archetype package means really you have to build something fairly custom for your particular type you are mapping to. Rather than squeeze too much into a comment though, thought the topic worth a post in itself.

As before, looking for a simple example, I've modelled a football match but I've changed it a little to use a nested Archetype. There's a top-level type that contains a single field set to which a text field and two nested score types can be added. The score type has fields for the team and the score.

From the content editor's perspective it looks like this:

I'm looking to map this to a view model that looks like this:

    public class FootballMatch
    {
        public FootballMatch()
        {
            Scores = new List<FootballMatchScore>();
        }

        public string Venue { get; set; }

        public IList<FootballMatchScore> Scores { get; set; }
    }

    public class FootballMatchScore
    {
        public string Team { get; set; }

        public int Score { get; set; }
    }

Which can then be used in the views like this:

    <h3>Today's Match at @Model.TodaysMatch.Venue</h3>

    @foreach (var score in Model.TodaysMatch.Scores)
    {
        <div>@score.Team @score.Score</div>
    }        

The trick as before is to register a custom mapping for the top-level type that will be mapped, like this:

    Mapper.AddCustomMapping(typeof(FootballMatch).FullName, ArchetypeMapper.MapFootballMatch);

And the implementation looks like this:

    public static object MapFootballMatch(IUmbracoMapper mapper, IPublishedContent contentToMapFrom, string propName, bool isRecursive)
    {
        // Get the strongly typed ArchetypeModel from the given property
        var archetype = contentToMapFrom.GetPropertyValue<ArchetypeModel>(propName, isRecursive);
        if (archetype != null)
        {
            // Pull out the first fieldset
            var fieldset = archetype.FirstOrDefault();
            if (fieldset != null)
            {
                // Instantiate an instance of type we are mapping to
                var result = new FootballMatch();

                // Convert the fieldset to a dictionary and map the top-level properties
                var fieldsetAsDictionary = fieldset.Properties.ToDictionary(
                     k => FirstCharToUpper(k.Alias),
                     v => v.Value);
                mapper.Map(fieldsetAsDictionary, result);

                // Given the known aliases for the nested type, loop through and map all the properties of each
                // of these too
                var aliases = new string[] { "homeTeam", "awayTeam" };
                foreach (var alias in aliases)
                {
                    var scoresModel = fieldset.GetValue<ArchetypeModel>(alias);

                    foreach (var innerFieldSet in scoresModel)
                    {
                        var innerFieldsetAsDictionary = innerFieldSet.Properties.ToDictionary(
                            k => FirstCharToUpper(k.Alias),
                            v => v.Value);
                        var score = new FootballMatchScore();
                        mapper.Map(innerFieldsetAsDictionary, score);
                        result.Scores.Add(score);
                    }
                }

                return result;
            }
        }

        return null;
    }

As I say, it would be nice to come up with something more generic to solve this type of problem, and we have managed this to an extent on a recent project that used a number of Archetypes. The flexibility of Archetype does perhaps come at a cost here from a developer perspective. It does seem fairly unavoidable that you'll have to do something quite custom depending on what exactly you are mapping from and too.

Nonetheless once done and set up as a custom mapping within the mapper, any instance of your view model type will be automatically mapped from the given Archetype.

No comments:

Post a Comment