How and Where to tell if a ViewComponent has been invoked x times in a view?

1.9k views Asked by At

I have a ViewComponent that I need to invoke twice only! How and where can I tell the invokations count?

Currently I can use a session but I dislike using session in mvc apps! How may I achieve this?

 namespace Partials.Components
 {
     public class MyComponent : ViewComponent
     {
         public IViewComponentResult Invoke()
         {
             Session["invoked"]=(int)Session["invoked"]+1;
             var model = new{
                        Website="Stack Overflow",
                        Url="www.http://stackoverflow.com"
                       };
             return View("_MyComponent ", model);
         }
     }
 }

and in my view

 @Component.Invoke("MyComponent")
 <span>Invoked ViewComponent <span>@Session["invoked"]</span>  times</span>
2

There are 2 answers

2
Daniel J.G. On BEST ANSWER

You can use HttpContext.Items which has the advantage of not using the session. These items are stored and shared per request, which would also fit your objective.

In your viewComponent you can add/retrieve an item as in this.Context.Items["MyComponentInvocationCount"]. Whenever the count is greater than 2 you can just return an empty content with return Content(String.Empty).

You can combine that with an extension method so you can get the count from outside that class:

[ViewComponent(Name = "MyComponent")]
public class MyViewComponent : ViewComponent
{
    internal static readonly string ContextItemName = "InvocationCount";

    public IViewComponentResult Invoke()
    {
        this.InvocationCount = this.InvocationCount + 1;
        if (this.InvocationCount > 2) return Content(String.Empty);

        //return your content here
        return Content("Can be invoked");
    }

    private int InvocationCount
    {
        get
        {
            return this.Context.InvocationCount();
        }
        set
        {
            this.Context.Items[ContextItemName] = value;
        }
    }
}

public static class MyViewComponentExtensions
{
    public static int InvocationCount(this HttpContext context)
    {
        var count = context.Items[MyViewComponent.ContextItemName];
        return count == null ? 0 : (int)count;
    }
}

Then you could use it in a view as follows:

@Component.Invoke("MyComponent")
<span>Invoked ViewComponent <span>@Context.InvocationCount()</span>  times</span>

If you add the above lines 3 times in a view, you will see that the third one does not add any content.


EDIT - Using ViewComponentInvoker

I have been exploring how to implement this feature adding a custom ViewComponentInvoker.

I started by adding a new attribute that can be used to decorate ViewComponents so they are limited to a certain number of invocations per request:

public class PerRequestInvocationLimitAttribute: Attribute
{
    public int PerRequestInvocationLimit { get; set; }
}

You would then create your view component as usual, the only change being adding this attribute:

[PerRequestInvocationLimit(PerRequestInvocationLimit = 2)]
public class MyViewComponent : ViewComponent
{
    //implementation of view component
}

We can then create a custom IViewComponentInvoker that decorates the DefaultViewComponentInvoker.

  • This custom view component invoker will keep track of the number of times a view component has been invoked in the current request.
  • When a view component that has the new attribute is invoked, it will only really invoke it if the number of invocations is below the limit.

Implementing this view component invoker looks like:

public class LimitedPerRequestViewComponentInvoker : IViewComponentInvoker
{
    private readonly IViewComponentInvoker _defaultViewComponentInvoker;
    public LimitedPerRequestViewComponentInvoker(IViewComponentInvoker defaultViewComponentInvoker)
    {
        this._defaultViewComponentInvoker = defaultViewComponentInvoker;
    }

    public void Invoke(ViewComponentContext context)
    {            
        if (!CanInvokeViewComponent(context)) return;
        this._defaultViewComponentInvoker.Invoke(context);
    }

    public Task InvokeAsync(ViewComponentContext context)
    {
        if (!CanInvokeViewComponent(context)) return Task.WhenAll();
        return this._defaultViewComponentInvoker.InvokeAsync(context);
    }

    private bool CanInvokeViewComponent(ViewComponentContext context)
    {
        // 1. Increase invocation count
        var increasedCount = context.ViewContext.HttpContext.IncreaseInvocationCount(
                                                                context.ViewComponentDescriptor.ShortName);

        // 2. check if there is any limit for this viewComponent, if over the limit then return false
        var limitAttribute = context.ViewComponentDescriptor.Type
                                        .GetCustomAttributes(true)
                                        .OfType<PerRequestInvocationLimitAttribute>()
                                        .FirstOrDefault();
        if (limitAttribute != null && limitAttribute.PerRequestInvocationLimit < increasedCount)
        {
            return false;
        }

        // 3. There is no limit set or the limit has not been reached yet
        return true;
    }
}

It uses some extension methods to set/get the invocation count from HttpContext.Items (That you could also use in your view to get the number of times a view component was invoked)

public static class ViewComponentExtensions
{
    public static int InvocationCount(this HttpContext context, string viewComponentName)
    {
        var count = context.Items[GetHttpContextItemsName(viewComponentName)];
        return count == null ? 0 : (int)count;
    }

    internal static int IncreaseInvocationCount(this HttpContext context, string viewComponentName)
    {
        var count = context.InvocationCount(viewComponentName);
        context.Items[GetHttpContextItemsName(viewComponentName)] = ++count;
        return count;
    }

    private static string GetHttpContextItemsName(string viewComponentName)
    {
        return string.Format("InvocationCount-{0}", viewComponentName);
    }
}

The final piece is to create a new IViewComponentInvokerFactory replacing the default one, so it creates an instance of the new custom view component invoker instead of the default one. You also need to register it on Startup.cs:

public class MyViewComponentInvokerFactory : IViewComponentInvokerFactory
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ITypeActivatorCache _typeActivatorCache;
    private readonly IViewComponentActivator _viewComponentActivator;

    public MyViewComponentInvokerFactory(IServiceProvider serviceProvider, ITypeActivatorCache typeActivatorCache, IViewComponentActivator viewComponentActivator)
    {
        _serviceProvider = serviceProvider;
        _typeActivatorCache = typeActivatorCache;
        _viewComponentActivator = viewComponentActivator;
    }

    public IViewComponentInvoker CreateInstance(ViewComponentDescriptor viewComponentDescriptor, object[] args)
    {
        return new LimitedPerRequestViewComponentInvoker(
            new DefaultViewComponentInvoker(_serviceProvider, _typeActivatorCache, _viewComponentActivator));
    }
}

//Configure the ViewComponentInvokerFactory in Startup.ConfigureServices
services.AddTransient<IViewComponentInvokerFactory, MyViewComponentInvokerFactory>();

With all these pieces in place, you can use your view component 3 times and you will see how it will be rendered only twice:

@Component.Invoke("MyComponent")
<span>Invoked ViewComponent <span>@Context.InvocationCount("MyComponent")</span>  times</span>

I prefer this solution for a few reasons:

  • It is based on the hooks provided by the new mvc framework.
  • Does not need changes to your view component, other than adding the attribute that sets the invocation limit.
  • It works when invoking view component asynchronously.
1
Ajay Bhargav On

You can use TempData. It persists only until the next request.

TempData["invoked"]=(int)TempData["invoked"]+1;

View:

<span>Invoked ViewComponent <span>@TempData["invoked"]</span>  times</span>

Note: TempData uses session under the covers.