What I want to do seems so simple.
In my index.cshtml I want to display the WizardStepAttribute
Value
So, a user will see at the top of each page, Step 1: Enter User Information
I have a ViewModel called WizardViewModel
. This ViewModel has a property that is IList<IStepViewModel> Steps
each "step" implements the Interface IStepViewModel, which is an empty interface.
I have a view called Index.cshtml. This view displays EditorFor()
the current step.
I have a custom ModelBinder, that binds the View to an new instance of the concrete class implementing IStepViewModel
based on the WizardViewModel.CurrentStepIndex
property
I have created a custom attribute WizardStepAttribute
.
Each of my Steps classes are defined like this.
[WizardStepAttribute(Name="Enter User Information")]
[Serializable]
public class Step1 : IStepViewModel
....
I have several problems though.
My View is strongly typed to WizardViewModel
not each step. I don't want to have to create a view for each concrete implementation of IStepViewModel
I thought I could add a property to the interface, but then I have to explicitly implement it in each class. (So this isn't any better)
I'm thinking I could implement it using reflection in the interface but, you can't refer to instances in methods in an interface.
It can be done, but it is neither easy nor pretty.
First, I would suggest adding a second string property to your WizardStepAttribute class, StepNumber, so that your WizardStepAttribute class looks like this:
[AttributeUsage(AttributeTargets.All, AllowMultiple = false)]
public class WizardStepAttribute : Attribute
{
public string StepNumber { get; set; }
public string Name { get; set; }
}
Then, each class must be decorated:
[WizardAttribute(Name = "Enter User Information", StepNumber = "1")]
public class Step1 : IStepViewModel
{
...
}
Next, you need to create a custom DataAnnotationsModelMetadataProvider, to take the values of your custom attribute and insert them into the Step1 model's metadata:
public class MyModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(
IEnumerable<Attribute> attributes,
Type containerType,
Func<object> modelAccessor,
Type modelType,
string propertyName)
{
var modelMetadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
var additionalValues = attributes.OfType<WizardStepAttribute>().FirstOrDefault();
if (additionalValues != null)
{
modelMetadata.AdditionalValues.Add("Name", additionalValues.Name);
modelMetadata.AdditionalValues.Add("StepNumber", additionalValues.StepNumber);
}
return modelMetadata;
}
}
Then, to present your custom metadata, I suggest creating a custom HtmlHelper to create your label for each view:
[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
public static MvcHtmlString WizardStepLabelFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
{
return WizardStepLabelFor(htmlHelper, expression, null /* htmlAttributes */);
}
[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
public static MvcHtmlString WizardStepLabelFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes)
{
return WizardStepLabelFor(htmlHelper, expression, new RouteValueDictionary(htmlAttributes));
}
[SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Users cannot use anonymous methods with the LambdaExpression type")]
[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
public static MvcHtmlString WizardStepLabelFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IDictionary<string, object> htmlAttributes)
{
if (expression == null)
{
throw new ArgumentNullException("expression");
}
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
var values = metadata.AdditionalValues;
// build wizard step label
StringBuilder labelSb = new StringBuilder();
TagBuilder label = new TagBuilder("h3");
label.MergeAttributes(htmlAttributes);
label.InnerHtml = "Step " + values["StepNumber"] + ": " + values["Name"];
labelSb.Append(label.ToString(TagRenderMode.Normal));
return new MvcHtmlString(labelSb.ToString() + "\r");
}
As you can see, the custom helper creates an h3 tag with your custom metadata.
Then, finally, in your view, put in the following:
@Html.WizardStepLabelFor(model => model)
Two notes: first, in your Global.asax.cs file, add the following to Application_Start():
ModelMetadataProviders.Current = new MyModelMetadataProvider();
Second, in the web.config in the Views folder, make sure to add the namespace for your custom HtmlHelper class:
<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<pages pageBaseType="System.Web.Mvc.WebViewPage">
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Routing" />
<add namespace="YOUR NAMESPACE HERE"/>
</namespaces>
</pages>
</system.web.webPages.razor>
Voila.
counsellorben
In our case we just needed an attribute that implements the IMetadataAware
interface:
https://msdn.microsoft.com/en-us/library/system.web.mvc.imetadataaware(v=vs.118).aspx
In your case, this could be:
public class WizardStepAttribute : Attribute, IMetadataAware
{
public string Name;
public void OnMetadataCreated(ModelMetadata metadata)
{
if (!metadata.AdditionalValues.ContainsKey("WizardStep"))
{
metadata.AdditionalValues.Add("WizardStep", Name);
}
}
}
来源:https://stackoverflow.com/questions/6834814/modelmetadata-custom-class-attributes-and-an-indescribable-question