The work that Microsoft is doing on the next release of the .NET framework and specifically ASP.NET 5 are very exciting. See here for a good summary. The changes for the most part eliminate all the arguments against using .NET as the platform of choice for a web development project, including:
- inability to run on operating systems other than windows
- slow development due to constant recompiling
- ghettoized community
The work is still in progress but everything is being done publicly on github and the developers are very receptive to working with members of the community as the product takes shape.
I've been using ASP.NET 5 / MVC 6 for a recent project and after a bit of a rocky start dealing with the learning curve (and lack of stable documentation as things are still changing quickly) I've come to be a real fan. Working with C# is about a thousand times better than working with Java, the standard library is second to none, and automatic recompilation as you type is a great productivity boost. Nevermind that PHP gets you that for free without anything nearly as sophisticated as the roslyn compiler, but I digress.
One thing that has been removed in MVC6 is the IMetadataAware
interface. This allowed you to create your own subclasses of Attribute that could add data to the ModelMetadata
structure to be used in an EditorTemplate or DisplayTemplate view. For instance, you could create a ReadOnlyAttribute:
public class ReadOnlyAttribute : Attribute, IMetadataAware
{
public void OnMetadataCreated(ModelMetadata Metadata)
{
Metadata.AdditionalValues["_IsReadOnly"] = true;
}
}
and then use that attribute on your model:
public class MyRecord
{
public int RecordID { get; set; }
[DisplayName("This value is readonly")]
[ReadOnly]
public string StaticValue { get; set; }
}
and in your EditorTemplate, the value would be available:
String.cshtml:
@model string
@{
bool IsReadOnly = ViewData.ContainsKey("_IsReadOnly") && (bool)ViewData["_IsReadOnly"];
}
@if (IsReadOnly) {
<div>@Model</div>
@* Don't draw a form field because the value is not editable *@
}
else {
@* Draw an input field, omitted for brevity *@
}
Since IMetadataAware
is removed from MVC6 I had to find another way to inject information into the view from model attributes. MVC6 does provide an IDisplayMetadataProvider
, which is just a class you can register that is provided the same ModelMetadata
context that IMetadataAware
was provided. However, this causes an architectural problem in that any implementation of IDisplayMetadataProvider
needs to know all about all the possible Attributes you have and what they mean, which is just bad design. Each attribute should be responsible for its own logic, as far as I'm concerned.
Luckily, it's not hard to recreate the old functionality of IMetadataAware
. I first created a new interface, IModelMetadataAware
:
public interface IModelMetadataAware
{
void GetDisplayMetadata(DisplayMetadataProviderContext context);
}
Any custom attributes that want to change ModelMetadata
can implement this. Here's the same ReadOnlyAttribute as before, implemented with IModelMetadataAware
:
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class ReadOnlyAttribute : Attribute, IModelMetadataAware
{
public void GetDisplayMetadata(DisplayMetadataProviderContext context)
{
context.DisplayMetadata.AdditionalValues.Add("_IsReadOnly", true);
}
}
Next, we need an implementation of IDisplayMetadataProvider
, one that has a private registry of attributes it should know about:
public class MyModelMetadataProvider : IDisplayMetadataProvider
{
private static List<Type> Registry = new List<Type>();
public static void RegisterMetadataAwareAttribute(Type Attribute)
{
if (Attribute == null)
{
throw new ArgumentNullException();
}
if (!typeof(IModelMetadataAware).IsAssignableFrom(Attribute))
{
throw new ArgumentException("This attribute type is not metadata aware.");
}
Registry.Add(Attribute);
}
public void GetDisplayMetadata(DisplayMetadataProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException();
}
foreach (Type Type in Registry)
{
IModelMetadataAware Attribute = (IModelMetadataAware)context.Attributes
.Where(x => x.GetType() == Type)
.FirstOrDefault();
if (Attribute != null)
{
Attribute.GetDisplayMetadata(context);
}
}
}
}
This IDisplayMetadataProvider
can be registered in Startup.cs of your MVC6 application:
public void ConfigureServices(IServiceCollection services)
{
[...snip...]
services.AddMvc()
.Configure<MvcOptions>(m => {
m.ModelMetadataDetailsProviders.Add(new MyModelMetadataProvider());
});
}
Finally, how do we register all the IModelMetadataAware
Types? Somewhere in Startup.cs, we can do this:
// Get all the model metadata aware attributes in the class
// and register them so they can add information to the metadata object.
var InterfaceType = typeof(IModelMetadataAware);
var CurrentAssembly = Assembly.GetAssembly(typeof(IModelMetadataAware));
var Types = CurrentAssembly
.GetTypes()
.Where(t =>
InterfaceType.IsAssignableFrom(t)
);
foreach (var Type in Types)
{
MyModelMetadataProvider.RegisterMetadataAwareAttribute(Type);
}
Assuming all the Attributes implementing IModelMetadataAware
are in a single assembly this should work fine, if not then it can be adjusted accordingly (see AppDomain.CurrentDomain.GetAssemblies()
).
Leave a comment -