Question

I have an ASP.NET MVC 3 application that uses custom attributes to create select controls for model properties that can be populated from external data sources at runtime. The issue is that my EditorTemplate output appear to be cached at the application level, so my drop down lists are not updated when their data source changes until the Application Pool is recycled.

I also have output the contents of the MVC 3 ActionCache that is bound to the ViewContext.HttpContext object as shown in the MVC 3 source code in System.Web.Mvc.Html.TemplateHelpers.cs:95.

  • Action Cache GUID: adf284af-01f1-46c8-ba15-ca2387aaa8c4:
  • Action Cache Collection Type: System.Collections.Generic.Dictionary``2[System.String,System.Web.Mvc.Html.TemplateHelpers+ActionCacheItem]
  • Action Cache Dictionary Keys: EditorTemplates/Select

So it appears that the Select editor template is definitely being cached, which would result in the TemplateHelper.ExecuteTemplate method to always return the cached value instead of calling ViewEngineResult.View.Render a second time.

Is there any way to clear the MVC ActionCache or otherwise force the Razor view engine to always re-render certain templates?

For reference, Here are the relevant framework components:

public interface ISelectProvider
{
    IEnumerable<SelectListItem> GetSelectList();
}

public class SelectAttribute : Attribute, IMetadataAware
{
    private readonly ISelectProvider _provider;

    public SelectAttribute(Type type)
    {
        _provider = DependencyResolver.Current.GetService(type) as ISelectProvider;
    }

    public void OnMetadataCreated(ModelMetadata modelMetadata)
    {
        modelMetadata.TemplateHint = "Select";
        modelMetadata.AdditionalValues.Add("SelectListItems", SelectList);
    }

    public IEnumerable<SelectListItem> SelectList
    {
        get
        {
            return  _provider.GetSelectList();
        }
    }       
} 

Next, there is a custom editor template in ~\Views\Shared\EditorTemplates\Select.cshtml.

@model object
@{
    var selectList = (IEnumerable<SelectListItem>)ViewData.ModelMetadata.AdditionalValues["SelectListItems"];
    foreach (var item in selectList) 
    {
        item.Selected = (item != null && Model != null && item.Value.ToString() == Model.ToString());
    }
}
@Html.DropDownListFor(s => s, selectList)

Finally, I have a view model, select provider class and a simple view.

/** Providers/MySelectProvider.cs **/
public class MySelectProvider : ISelectProvider
{
    public IEnumerable<SelectListItem> GetSelectList()
    {
        foreach (var item in System.IO.File.ReadAllLines(@"C:\Test.txt"))
        {
            yield return new SelectListItem() { Text = item, Value = item };
        } 
    }
}

/** Models/ViewModel.cs **/
public class ViewModel
{
    [Select(typeof(MySelectProvider))]
    public string MyProperty { get; set; }
}

/** Views/Controller/MyView.cshtml **/
@model ViewModel
@using (Html.BeginForm())
{
    @Html.EditorForModel()
    <input type="submit" value="Submit" />
}

** EDIT **

Based on suggestions in the comment, I started to look more closely at the ObjectContext lifecycle. While there were some minor issues, the issue appears to be isolated to an odd behavior involving a callback within a LINQ expression in the SelectProvider implementation.

Here is the relevant code.

public abstract class SelectProvider<R, T> : ISelectProvider
  where R : class, IQueryableRepository<T>
{
    protected readonly R repository;

    public SelectProvider(R repository)
    {
        this.repository = repository;
    }

    public virtual IEnumerable<SelectListItem> GetSelectList(Func<T, SelectListItem> func, Func<T, bool> predicate)
    {
        var ret = new List<SelectListItem>();
        foreach (T entity in repository.Table.Where(predicate).ToList())
        {
            ret.Add(func(entity));
        }

        return ret;
    }

    public abstract IEnumerable<SelectListItem> GetSelectList();
}


public class PrinterSelectProvider : SelectProvider<IMyRepository, MyEntityItem>
{
    public PrinterSelectProvider()
        : base(DependencyResolver.Current.GetService<IMyRepository>())
    {
    }

    public override IEnumerable<SelectListItem> GetSelectList()
    {
        // Create a sorted list of items (this returns stale data)
        var allItems = GetSelectList(
            x => new SelectListItem()
            {
                Text = x.DisplayName,
                Value = x.Id.ToString()
            },
            x => x.Enabled
        ).OrderBy(x => x.Text);

        // Do the same query, but without the callback
        var otherItems = repository.Table.Where(x => x.Enabled).ToList().Select(x => new SelectListItem()
            {
                Text = x.DisplayName,
                Value = x.Id.ToString()
            }).OrderBy(x => x.Text);

        System.Diagnostics.Trace.WriteLine(string.Format("Query 1: {0} items", allItems.Count()));
        System.Diagnostics.Trace.WriteLine(string.Format("Query 2: {0} items", otherItems.Count()));

        return allItems;
    }
}

And, the captured output from the System.Diagnostics.Trace is

Query 1: 2 items
Query 2: 3 items

I'm not sure what could be going wrong here. I considered that the Select may need an Expressions, but I just double-checked and the LINQ Select method only takes Func objects.

Any additional suggetions?

Was it helpful?

Solution

Problem Solved!

I finally had a chance to re-visit this issue. The root cause had nothing to do with LINQ, the ActionCache, or the ObjectContext, rather it was related to when attribute constructors are called.

As shown, my custom SelectAttribute class calls DependencyResolver.Current.GetService in its constructor to create an instance of the ISelectProvider class. However, the ASP.NET MVC framework scans the assemblies for custom metadata attributes once and keeps a reference to them in the application scope. As explained in the linked question, accessing a Attribute triggers its constructor.

So, the constructor was run only once, rather than on each request, as I had assumed. This meant that there was actually only one, cached instance of the PrinterSelectProvider class instantiated that was shared across all requests.

I solved the problem by changing the SelectAttribute class like this:

public class SelectAttribute : Attribute, IMetadataAware
{
    private readonly Type type;

    public SelectAttribute(Type type)
    {
        this.type = type;
    }

    public void OnMetadataCreated(ModelMetadata metadata)
    {
        // Instantiate the select provider on-demand
        ISelectProvider provider = DependencyResolver.Current.GetService(type) as ISelectProvider;

        modelMetadata.TemplateHint = "Select";
        modelMetadata.AdditionalValues.Add("SelectListItems", provider.GetSelectList());
    }
}

Tricky problem indeed!

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top