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?