Copied to clipboard

Flag this post as spam?

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


  • andrew shearer 465 posts 579 karma points
    Jul 22, 2021 @ 04:33
    andrew shearer
    0

    Umbraco context in ViewComponent: best practice

    Hi everyone

    Partials don’t exist in the .net core world, now its all about View Components. So If my scenario was having a “header” View Component in my Layout file (so its shared across all site templates), and the menu shown in this header was managed on a “homepage” node in the CMS, what would be the best way of obtaining this property for the viewModel? i.e. this in my umbraco 8 site:

    @Html.Action("Header", "MyController")
    

    becomes this in umbraco 9:

    <vc:header />
    

    Do I use dependency injection to give it some Umbraco Context? https://our.umbraco.com/documentation/reference/using-ioc/ hasn’t been updated for umbraco 9 so haven’t found a good example for doing this yet.

    Or can I access the ModelsBuilder classes somehow? i.e. the homepage model and the “menu” property from that

    Or another better method?

    Thanks

    Andrew

  • Garcia562 1 post 71 karma points
    Jul 22, 2021 @ 05:29
    Garcia562
    0

    So it seems like ViewComponents could be pretty cool within the context of Umbraco, you can inject services into their constructor using di (think umbraco services) and pass in additonal parameters (like macros) and they aren't part of the request eg like child actions - but it seems using either invoke or invokeasync, you can conjure up a model in C# and send it through to a View.

  • Benjamin Carleski 33 posts 291 karma points MVP c-trib
    Jul 22, 2021 @ 17:34
    Benjamin Carleski
    101

    Yes, you can use dependency injection. According to the Microsoft Docs, all view components can use dependency injection in the constructor, so you would inject an Umbraco Context just like you would in any other location. You'd have a constructor argument of type IUmbracoContextAccessor, and then save that to a private, readonly field. Then in your InvokeAsync method, you would get the Umbraco Context from the accessor field and use it to get your homepage node. You could cast the IPublishedContent for the home page node to the Models Builder type to make it simpler to access the properties you needed, or you could use the .Value method on IPublishedContent. Something like the following should work:

    public class MyViewComponent : ViewComponent
    {
        private readonly IUmbracoContextAccessor _umbracoContextAccessor;
    
        public MyViewComponent(IUmbracoContextAccessor umbracoContextAccessor)
        {
            _umbracoContextAccessor= umbracoContextAccessor;
        }
    
        public async Task<IViewComponentResult> InvokeAsync()
        {
            var ctx = _umbracoContextAccessor;
            var home = ctx.Content.GetAtRoot().FirstOrDefault() as Homepage;
            var menu = home?.Menu;
            return View(menu);
        }
    }
    
  • andrew shearer 465 posts 579 karma points
    Jul 22, 2021 @ 23:22
    andrew shearer
    0

    thanks Benjamin! yes it was the IUmbracoContextAccessor documentation that i couldn't find. thank you

  • andrew shearer 465 posts 579 karma points
    Aug 19, 2021 @ 22:20
    andrew shearer
    1

    fyi it seems release candidate 2 has broken the above and reinstated the umb8 way of doing things:

        public class MyViewComponent : ViewComponent
    {
        private readonly IUmbracoContextFactory _context;
    
        public MyViewComponent(IUmbracoContextFactory context)
        {
            _context = context;
        }
    
        public async Task<IViewComponentResult> InvokeAsync()
        {
            using (var cref = _context.EnsureUmbracoContext())
            {
                //use context here
                return View(viewModel);
            }   
        }
    }
    

    :)

  • Benjamin Carleski 33 posts 291 karma points MVP c-trib
    Aug 19, 2021 @ 22:39
    Benjamin Carleski
    0

    Broken it how? Does IUmbracoContextAccessor no longer work, or just not for the context you are working in? The IUmbracoContextFactory was always an option, so I'm not seeing what was reverted to the v8 way.

    Also, I don't know if it is the same with v9, but I know in v8 you should generally use a Scope if you were going to use IUmbracoContextFactory. Shannon talked about it at https://our.umbraco.com/forum/using-umbraco-and-getting-started/102676-triggering-index-rebuild-via-hangfire-causes-objectdisposedexception-in-nucache#comment-321129, but I'm not sure if that applies anymore or not.

  • andrew shearer 465 posts 579 karma points
    Aug 19, 2021 @ 22:50
    andrew shearer
    0

    "broken it" as in the example you supplied worked in RC1 but has a compile error in RC2

    enter image description here

  • andrew shearer 465 posts 579 karma points
    Aug 20, 2021 @ 04:05
    andrew shearer
    0

    It would still be great if https://our.umbraco.com/documentation/reference/using-ioc/ could be updated for Umbraco 9. I'm still trying to figure out the best-practice way of getting the "current" umbraco content page within my custom ViewComponent. When I try to inject UmbracoHelper via DI and use "AssignedContentItem" i get the following runtime error:

    InvalidOperationException: Cannot return the IPublishedContent because the UmbracoHelper was not constructed with an IPublishedCont
    

    thanks

        public class MetaTagsViewComponent : ViewComponent
    {
        private readonly UmbracoHelper _umbraco;
    
        public MetaTagsViewComponent(UmbracoHelper umbraco)
        {
            _umbraco = umbraco;
        }
    
        public async Task<IViewComponentResult> InvokeAsync()
        {
    
            var current = _umbraco.AssignedContentItem;
            return View();
    
        }
    }
    
  • andrew shearer 465 posts 579 karma points
    Aug 23, 2021 @ 03:00
    andrew shearer
    0

    What would actually be great in .net 5 is if there was a base ViewComponent class ("UmbracoViewComponent"?) that automatically provided Umbraco context and helpers for free, similar to how "RenderMvcController" did in the .net 4 world. Thoughts?

  • AbsolutelyN 53 posts 336 karma points
    Oct 05, 2021 @ 08:21
    AbsolutelyN
    0

    The following works for me.

    public class MyComponent : ViewComponent
    {
    
        private readonly IUmbracoContextAccessor _umbracoContextAccessor;
    
        public MyComponent(IUmbracoContextAccessor umbracoContextAccessor)
        {
            _umbracoContextAccessor = umbracoContextAccessor;
        }
    
    
        public IViewComponentResult Invoke()
        {
            var myModel = new MyModel();
            if (_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext context))
            {
                // use context here
            }
    
            return View(myModel);
        }
    
    }
    
  • Jamie Attwood 174 posts 427 karma points c-trib
    Nov 23, 2021 @ 20:02
    Jamie Attwood
    0

    I understand best practices of MVC Patterns as that relates to testability and separation of concerns, but it's unlikely that you would ever be using a viewcomponent without there being umbraco context.

    Can't you just pass in your current page model as an IPublishedContent parameter to your ViewComponent like this? (Using my breadcrumb ViewComponent here as an example). This way you feed it everything it needs to know from the view without having to jump through hoops in your VC... seems a lot simpler to me. Just not sure of the extra overhead if any....

       @(await Component.InvokeAsync("Breadcrumb", new { currentPage = Model}))
    

    and then in your View component:

    public IViewComponentResult Invoke(IPublishedContent currentPage)
            {
                List<Crumb> crumbsList = CrumbBuilder(currentPage);
    
                var model = new BreadcrumbViewModel()
                {
                    Crumbs = crumbsList,
                    CurrentPageName = currentPage.Value<string>("browserTitle") != null ? currentPage.Value<string>("browserTitle") : currentPage.Name
                };
                return View("Index", model);
            }
    

    [Edit] Playing around with this further, if you are working with a view model inside your view component that inherits from a models builder model, to inject the content into the model, you now need to pass the PublishedValueFallback as well. So just passing the model from the view as a parameter won't work, so back to DI. If you inject the IUmbracoContextAccessor then you can get the current page in one line like this:

    var currentPage = _context.GetRequiredUmbracoContext().PublishedRequest.PublishedContent;
    
  • Jules 214 posts 461 karma points
    26 days ago
    Jules
    0

    Since you have to inject IUmbracoContextAccessor and then use the following code to to get UmbracoContext.

    _umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext);
    

    I am thinking it may be better to create a service to get the UmbracoContext and then inject that wherever you need.

    namespace Services.UmbracoContextService
    {
        public  interface IUmbracoContextService
        {
            public IUmbracoContext GetUmbracoContext();
        }
    
        public class UmbracoContextService : IUmbracoContextService
        {
            private readonly IUmbracoContextAccessor _umbracoContextAccessor;
    
            public UmbracoContextService(IUmbracoContextAccessor umbracoContextAccessor)
            {
                _umbracoContextAccessor = umbracoContextAccessor;
            }
    
            public IUmbracoContext GetUmbracoContext()
            {
                _umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext);
    
                return umbracoContext;
            }
        }
    }
    

    Now I can inject that service into another service, ViewComponent etc making code reusable and less cluttered.

    private readonly IUmbracoContext _umbracoContext;
    
    public MyService(IUmbracoContextService umbracoContextService)
    {
        _umbracoContext = umbracoContextService.GetUmbracoContext();
    }
    

    Any downsides? Is there a better way to do this? Would be interested in opinions.

    J

  • Jamie Attwood 174 posts 427 karma points c-trib
    22 days ago
    Jamie Attwood
    0

    I like that idea, but personally, I think it would be best to inject IUmbracoContextAccessor into the component or controller that you need it in, since I believe that TryGetUmbracoContext tries to obtain the UmbracoContext by first checking to see if one exists on the current thread. I am not sure how that trancends into a service that is getting injected without context in and of itself.

    Since you still need to inject your new service into those classes the same way as injecting IUmbracoContextAccessor, you are not really saving too much repetition from a DRY sense in my opinion.

    If it's a view component or controller, you are pretty much ensured that there is going to be context and I would just get the context via GetRequiredUmbracoContext().

    var currentPage = _context.GetRequiredUmbracoContext().PublishedRequest.PublishedContent;
    

    If it's a service that it's possible to instantiate without a component or controller, then I would go the full route and ensure that context is present using:

        using (var cref = _context.EnsureUmbracoContext())
                {
     //Do stuff   
                }   
    

    Either way, if you use your service, you should probably test whether there is context returned to your component/controller anyway because if your service returns null, it's still going to break stuff.

    Anyway these are all personal opinions.

    Cheers,

    Jamie

Please Sign in or register to post replies

Write your reply to:

Draft