开发者

ModelMetaData, Custom Class Attributes and an indescribable question

开发者 https://www.devze.com 2023-03-22 21:19 出处:网络
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

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 prop开发者_JAVA技巧erty

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);
        }
    }
}
0

精彩评论

暂无评论...
验证码 换一张
取 消