Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • Paul Griffiths 334 posts 933 karma points
    Oct 21, 2019 @ 18:07
    Paul Griffiths
    0

    Best way to pass content model values using custom view model and controller

    Hi,

    Following https://our.umbraco.com/documentation/reference/routing/custom-controllers i I have been experimenting with Hijacking Umbraco Routes for my blog pages. I wanted to build up the latest blog posts, comments and add an author profile, so figured i needed a new viewModel. I created the following

    View Model

    public class BlogPageViewModel : ContentModel
    {
        public BlogPageViewModel(IPublishedContent content) : base(content)
        {
    
        }
    
        public int Id { get; set; }
        public string Heading { get; set; }       
        public string MainContent{ get; set; }   
        public IEnumerable<string> Categories { get; set; }
        public IEnumerable<string> Tags { get; set; }
        public AuthorProfile AuthorProfile { get; set; }
        public IEnumerable<BlogPost> LatestBlogPosts { get; set; }
        public BlogCommentsViewModel BlogComments { get; set; }
    }
    

    Controller

            public override ActionResult Index(ContentModel model)
        {
            var blogPage = model.Content as BlogPage;
            var authorPage = blogPage.Author as AuthorPage;
    
            var authorProfile = BuildAuthorProfile(authorPage);
            var latestBlogPosts = BuildLatestBlogs(blogPage);
            var blogComments = BuildBlogComments(blogPage);
    
            var vm = new BlogPageViewModel(model.Content)
            {
                Id = blogPage.Id,
                Heading = blogPage.Heading,
                MainContent = blogPage.MainContent,
                Tags = blogPage.Tags,
                Categories = blogPage.Categories,
                AuthorProfile = authorProfile,
                LatestBlogPosts = latestBlogPosts,
                BlogComments = blogComments
            };
    
            return CurrentTemplate(vm);
        }
    

    My view is strongly typed to BlogPageViewModel which means i can do Model.ID, Model.MainContent, Model.BlogComments etc. to get the values which works great. However, with things like ID, Heading, MainContent they all exist on the BlogPage ContentModel so should i be doing something like the following in my viewmodel, add a public property called BlogPage

     public BlogPageViewModel(IPublishedContent content) : base(content)
        {
    
        }
    
        //public int Id { get; set; }
        //public string Heading { get; set; }        
        public BlogPage BlogPage { get; set; }
        public string GridContentAlias { get; set; }
        //public IEnumerable<string> Categories { get; set; }
        //public IEnumerable<string> Tags { get; set; }
        public AuthorProfile AuthorProfile { get; set; }
        public IEnumerable<BlogPost> LatestBlogPosts { get; set; }
        public BlogCommentsViewModel BlogComments { get; set; }
    } 
    

    assign it in my controller as follows

     public override ActionResult Index(ContentModel model)
            {
                var blogPage = model.Content as BlogPage;
                var authorPage = blogPage.Author as AuthorPage;
    
                var authorProfile = BuildAuthorProfile(authorPage);
                var latestBlogPosts = BuildLatestBlogs(blogPage);
                var blogComments = BuildBlogComments(blogPage);
    
                var vm = new BlogPageViewModel(model.Content)
                {
                    //Id = blogPage.Id,
                    //Heading = blogPage.Heading,
                    //MainContent = blogPage.MainContent,
                    //Tags = blogPage.Tags,
                    //Categories = blogPage.Categories,
                    BlogPage = blogPage
                    AuthorProfile = authorProfile,
                    LatestBlogPosts = latestBlogPosts,
                    BlogComments = blogComments
                };
    
                return CurrentTemplate(vm);
            }
    

    Then in my view to access the content models i would do the following Model.BlogPage.Id, Model.BlogPage.MainContent, Model.BlogPage.Heading, Model.BlogPage.Tags etc.

    If feel like its pointless me adding the ID, Heading etc. to my VM when i can pass it all via the BlogPage property. Been undecided whether to post this or not and whether its a silly question but Im getting a little hung up on which approach to take or whether i should be doing it another way?

    I mean both work but i want to be learning and applying the best practices.

    Any advice would be greatly appreciated.

    Paul

  • Markus Johansson 1629 posts 4589 karma points
    Oct 22, 2019 @ 08:13
    Markus Johansson
    100

    Hi Paul!

    Lovely question, don't ever hesitate to ask things like this as this is the way we all share and learn!

    I'll share my thoughts around this and to start off I would like to mention the idea behind MVC and the separation that it advocates. The idea would be that your views should not be "tied" to any specifics around the under laying system. To achieve this with Umbraco you would typically have to do a lot of "boilerplate" code just to get the foundation for this, your views would not know anything about UmbracoViewPage or IPublishedContent (or any of you Model Builder models), you would have to replicate all the properties that Models Builder creates for us etc. etc. My opinion around this approach is that it only introduces complexity and lots of unnecessary work, and how many times have you made a switch of CMS without making major changes to the website implementation? Never happened to me. So I'll leave the idea of "abstracting away" Umbraco from the views and put focus on how I would work in your scenario.

    When it comes to the models your view models are inheriting from ContentModel, I guess that this is since that the article you linked to is purposing but you don't need to return a ContentModel to the view - you could also return a IPublishedContent.

    What I would do is to have my view model inherit from the Models Builder type I'm working with. In your case the BlogPage.

    public class BlogPageViewModel : BlogPage
    {
       public BlogPage(IPublishedContent content) : base(content)
       {
       }
    
       public AuthorProfile AuthorProfile { get; set; }
       public IEnumerable<BlogPost> LatestBlogPosts { get; set; }
       public BlogCommentsViewModel BlogComments { get; set; }
    }
    

    This way all the properties on the BlogPage is automaticlly exposed when my view model is used. I'm guessing that your view is expecting an instance of "BlogPageViewModel" ( or at least it should =D )

    @inherits Umbraco.Web.Mvc.UmbracoViewPage<BlogPageViewModel>
    

    This means that you can pass the IPublishedContent-item (being the BlogPage-instance) to the view from your controller.

    public override ActionResult Index(ContentModel model)
    {
        var vm = new BlogPageViewModel(model.Content)
    
       var authorProfile = BuildAuthorProfile(vm.AuthorPage);
       var latestBlogPosts = BuildLatestBlogs(vm);
       var blogComments = BuildBlogComments(vm);
    
        vm.AuthorProfile = authorProfile,
        vm.LatestBlogPosts = latestBlogPosts,
        vm.BlogComments = blogComments        
    
        return CurrentTemplate(vm);
    }
    

    This way the properties created on the BlogPage by Models Builder will follow you all the way into the view and you'd only need to run code to get things like the latest blog posts, author profile etc.

    This is the approach that I would choose in most cases, but there are some things that I would like to point out:

    • Since you are exposing the IPublishedContent-item (BlogPage) in the view there's room for a lot of logic in the views, it's a good practice to avoid this logic in the views. Both for performance and for maintainability. Put them in the controller or maybe even call some custom "builder class" from your controller - this promotes re usability in other controllers.

    • Exposing the latest blog posts using the "full" IPublishedContent-model here might be overkill, I often use something like a BlogPostListItemViewModel and copy over just the properties I need in the view. This has the benefit of making sure that we're not making strange sub-queries in the listing of blog pages and also that these BlogPostListItemViewModels could be cached if needed. Same thing goes for comments and AuthorProfile. Generally speaking I try to only expose the full IPublishedContent-model for the "main content item" that the view is built for, any lists etc would have it's own type of models, for re-usability and caching purposes. I do admit that this might sometimes be overkill so don't take this as a "rule" just as something to keep in mind, it's also quite easy to refactor towards a special ListItemViewModels if needed in the future.

    Hope this helps you and that it in some way answers your questions =D

  • Paul Griffiths 334 posts 933 karma points
    Oct 22, 2019 @ 11:25
    Paul Griffiths
    0

    Hello Markus,

    Thank you very much for taking the time to respond and more importantly providing a comprehensive response with explanations. This is really really helpful.

    With regards to my view model, yes, to be completely honest the only reason i inherited from ContentModel is because it was being used in the example documentation that i shared. In my quest to find out if what i was doing was correct, I delved into the inheritance chain for ContentModel (IContentModel) and also looked and the Articulate Source code (https://github.com/Shazwazza/Articulate/blob/master/src/Articulate/Models/MasterModel.cs) and seen that Shannon was inheriting from PublishedContentWrapped. This all left me a little confused, hence my forum post.

    To expose the BlogPage properties on my VM, ensuring that it inherits from BlogPage makes perfect sense now! However, i have strongly typed my view to BlogPageViewModel

    *(but i understand a BlogPageViewModel 'is a' BlogPage)

    @inherits Umbraco.Web.Mvc.UmbracoViewPage<BlogPageViewModel>
    

    Yeah, I appreciate that exposing the IPublishedContent item (BlogPage) in the view provides an opportunity for more logic in the views and i am trying to avoid this to keep them as clean as possible. With this in mind it did prompt another question regarding images and the cropper. For my images i always like to fallback to a static image so i was adding a public property to my VM called ImageUrl and doing something like so in my controller

            public override ActionResult Index(ContentModel model)
        {
            var blogPage = model.Content as BlogPage;
            var authorProfile = new AuthorProfile();
    
            var imageUrl = blogPage.Image != null ?
                Url.GetCropUrl(blogPage, "image", "mainImage").ToString()
                : Global.PlaceholderImage.MainImage;
    

    In my view i was simply populating the img source with

    <img src="Model.ImageUrl"
    

    However, after reading https://our.umbraco.com/Documentation/Getting-Started/Backoffice/Property-Editors/Built-in-Property-Editors/Image-Cropper and seeing that the new Url.GetCropUrl helper (which is not available in classes that dont inherit umbarco core classes such as RenderMvcController, SurfaceController ) should be used it got me thinking whether i should be adding my image logic in the view? Again both ways work but its the uncertainty of which way is best, or is there another.

    Point understood about exposing the latestBlogs using the full IPublishedContentModel and this makes sense! I am super keen to learn and understand the best practices and that's why i'm always keen to know\understand how others would approach things.

    Again, i thank you for sharing your thoughts regarding this - its been super helpful :)

    Paul

  • Markus Johansson 1629 posts 4589 karma points
    Oct 22, 2019 @ 11:50
    Markus Johansson
    1

    Hi Paul!

    Thanks for correcting the mistakes in my examples, of course the BlogPageViewModel should be the right thing to use in the view. (I've updated the examples as well if anyone reads them and not your answer =D )

    To be clear reg. the view, the BlogPageViewModel is a BlogPage (by the inheritance) and my example was wrong, you should use BlogPageViewModel in the view.

    Reg. the use of ContenModel in Articulate and the article in the docs, to be honest I'm not sure if there is anything "wrong" with what I'm proposing here, I can't really see any downside but I've raised an issue here https://github.com/umbraco/UmbracoDocs/issues/2026 to clear things out but we're currently using this approach in multiple projects so it does work =D

    Reg. your question around the images, there's extension methods for this on the "IPublishedContent" item as well: https://github.com/umbraco/Umbraco-CMS/blob/d2cde44b1f17be5217822173197bf40d10ab4984/src/Umbraco.Web/ImageCropperTemplateExtensions.cs#L52

    So i think you should be able to use this extention on the media item it self to get the Crop, something like:

     var imageUrl = blogPage.Image != null ?
            blogPage.Image.GetCropUrl("cropAliasName")
            : Global.PlaceholderImage.MainImage;
    

    Cheers!

  • Paul Griffiths 334 posts 933 karma points
    Oct 22, 2019 @ 12:32
    Paul Griffiths
    0

    Hi Markus,

    No problems, just want to ensure that i am following along :).

    Yeah i fully appreciate that there is usually no right or wrong and different scenarios may lead to a different approach. However, its good to have a clear understand on the standard approach which i usually find in the docs. Ill keep an eye on the issue you have raised - ill be interested in the discussion around this.

    In the meantime, i can take away the information that you provided (including the image info) and start refactoring my code a little. If you have any decent links to documentation or videos on this topic please do send them my way.

    Some reading material i have been getting my head into are:

    Thanks Paul

Please Sign in or register to post replies

Write your reply to:

Draft