umco / umbraco-ditto

Ditto - the friendly view-model mapper for Umbraco
http://our.umbraco.org/projects/developer-tools/ditto
MIT License
79 stars 33 forks source link

How to access the current item being mapped .As<T>()? #153

Closed LoganLehman closed 8 years ago

LoganLehman commented 8 years ago

I am running into somewhat of a complex problem with the As() extension method. Before I move forward to explain the issue I am going to list a small dictionary of terms I will be using throughout this ticket:

  1. Article - Document Type Composition for Article DocType
  2. Article DocType - Self explanatory - Uses Article Composition
  3. PageConverter - Generic TypeConverter I created that will return a particular content item from a passed ID.
  4. HeaderConverter - TypeConverter that will return either the header property or node name (on current context's assigned content item).

Now the problem:

I have a page (one of our typical document types) that contains an Archetype called ArticlesBanner. This ArticlesBanner has the following function:

On the left side of the banner, it will show a summary set on the Archetype, as well as breadcrumbs leading up to the current page that the Archetype is on, as well as the header of the current page. On the right side of the banner, it will show the header, summary, and category of an Article page that is picked with a multinode treepicker The code for this banner looks like this:

    /// <summary>
    /// The articles banner widget.
    /// </summary>
    [ArchetypeContent(alias: "articlesBanner")]
    public class ArticlesBanner: IWidget 
    {

        #region Properties

        /// <summary>
        /// The banner's header (override optional).
        /// </summary>
        [TypeConverter(typeof(HeaderConverter))]
        public string Header { get; set; }

        /// <summary>
        /// The banner's summary.
        /// </summary>
        public string Summary { get; set; }

        /// <summary>
        /// The banner's breadcrumbs.
        /// </summary>
        [ArchetypeProperty(alias: "labelIgnored")] // Random property so that ditto knows to map it 
        [TypeConverter(typeof(BreadcrumbConverter))]
        public List<Link> Breadcrumbs { get; set; }

        /// <summary>
        /// The banner's picked article details
        /// </summary>
        [ArchetypeProperty(alias: "article")]
        [TypeConverter(typeof(PageConverter<Article>))]
        public Article Article { get; set; }

        #endregion

    }

The Article class (Article.cs) holds the details for the Article Composition. It looks like this:

    /// <summary>
    /// Article details for article pages.
    /// </summary>
    public class Article
    {

        #region Properties

        /// <summary>
        /// The header of the article page.
        /// </summary>
        /* ISSUE: Return header from specified ID because Ditto is mapping calling page header
        rather than the called page header (via picker)*/
        [UmbracoProperty(propertyName: "id")] 
        [TypeConverter(typeof (HeaderConverter))]
        public string Header { get; set; }

        /// <summary>
        /// The summary of the article page.
        /// </summary>
        public string Summary { get; set; }

        /// <summary>
        /// The parent article's author.
        /// </summary>
        [TypeConverter(typeof(PageConverter<Author>))]
        public Author Author { get; set; }

        /// <summary>
        /// The parent articles's release date.
        /// </summary>
        public DateTime ReleaseDate { get; set; }

        /// <summary>
        /// The URL of the article page.
        /// </summary>
        public string URL { get; set; }

        /// <summary>
        /// The categories of the article page.
        /// </summary>
        [TypeConverter(typeof(PrevalueCSVConverter))]
        public List<string> Categories { get; set; }

        #endregion

    }

If you see on the Article, I am using [UmbracoProperty(propertyName: "id")]. This is because the behavior I am experiencing is that the value I get back from from this code (Inside HeaderConverter):

var node = UmbracoContext.Current.PublishedContentRequest.PublishedContent;

is the the origin content (typical page), and not the content of Article page that I have picked in multinode treepicker (where it should be mapping to using .As<Article>() inside of my PageConverter):

        /// <summary>
        /// Returns TPage from ID.
        /// </summary>
        /// <returns></returns>
        public override object ConvertFrom(ITypeDescriptorContext context,
            CultureInfo culture, object value)
        {

            // Return <T>Page
            if (value != null) {
                // Helper method to return UmbracoHelper
                var content = Rhythm.Extensions.Helpers.ContentHelper.GetHelper().TypedContent(Int32.Parse((string)value));
                return content.As<T>();
            }

            return null;
        }

So It seems to me that the mapper doesn't change the context's current content (either by UmbracoHelper.AssignedContentItem or PublishedContentRequest.PublishedContent) when mapping referenced types through content pickers.

Because this is the case, how can I get access the current item that is mapping from a TypeConverter?

leekelleher commented 8 years ago

HI @LoganLehman

In v0.8x, there is an undocumented feature in which we add in a custom ITypeDescriptorContext implementation to TypeConverters that are used by Ditto. The Instance property on that context is the current node that is being mapped.

Try this code...

var dittoCtx = (DittoTypeConverterContext)context;
var content = (IPublishedContent)dittoCtx.Instance;

An alternative way to do this is instead of using [UmbracoProperty(propertyName: "id")], to get the ID handle that in the TypeConverter, try using the [CurrentContentAs] attribute. This will pass the current content node back to Ditto for further mapping.

I hope this helps.

Cheers, Lee.

leekelleher commented 8 years ago

@LoganLehman Out of curiosity, what is this line of code for?

[ArchetypeProperty(alias: "labelIgnored")] // Random property so that ditto knows to map it 
LoganLehman commented 8 years ago

Thanks so much for the help! I actually noticed that [CurrentContentAs] did pass the "header" property, but because I am defaulting to the node name if "header" doesn't have a value, it did not work because I was not able to access the mapping node, only the node in the current context. Now that I am able to access the mapping node, I can use CurrentContentAs instead of the id.

In regards to your second question, it is a Ditto.Resolvers ArchetypeProperty attribute (which I am assuming you know about since it is an extension off of Ditto). "labelIgnored" actually just acts as a BackOffice label for the Archetype.

I was actually going to submit a separate ticket for it, but because you asked:

Because Breadcrumbs doesn't exist in the Archetype to begin with, I had to add that attribute to force the TypeConverter to run. I believe this is truly just an architecture decision on the team lead's end because he wants all the heavy lifting to occur in a TypeConverter. It would make sense that if the property exists only in the POCO/Model, and not in Umbraco, that it would not even think about running the TypeConverter because it won't have a value to convert from. So essentially, we are converting from nothing to something (with a trick).

If there is a way to do this in Ditto without doing this, that would be cool. Either way, we are just trying to stay away from View logic as much as possible.

leekelleher commented 8 years ago

@LoganLehman Thanks for the explanation, I can understand the reason.

An alternative approach is to use the conversion-handlers, specifically the [DittoOnConverted] attribute, (unfortunately our documentation for these is desperately lacking at the moment - sorry) .. for an example, take a look at our unit-test: ConversionHandlerTests.cs

Cheers, Lee.