2

I have the following situation - I need to write a custom additional metadata attribute, that based on another property value (from the same model), adds a value to the AdditionalValues dictionary. Right now, my issue is that I'm not able to access the model instance inside my attribute class.

[AttributeUsage(AttributeTargets.Property)]
public class ExtendedAdditionalMetadataAttribute : Attribute, IMetadataAware
{
    #region Private properties
    private string extraFieldToCheck { get; set; }

    private string extraFieldValueToCheck { get; set; }

    private string fieldToBeAdded { get; set; }

    private string fieldValueToBeAdded { get; set; }
    #endregion

    #region Constructor
    public ExtendedAdditionalMetadataAttribute(string extraFieldToCheck, string extraFieldValueToCheck,
        string fieldToBeAdded, string fieldValueToBeAdded)
    {
        this.extraFieldToCheck = extraFieldToCheck;
        this.extraFieldValueToCheck = extraFieldValueToCheck;
        this.fieldToBeAdded = fieldToBeAdded;
        this.fieldValueToBeAdded = fieldValueToBeAdded;
    }
    #endregion

    public void OnMetadataCreated(ModelMetadata metadata)
    {
        // HOW TO GET THE MODEL CLASS INSTANCE??? 
        // metadata.ContainerType is correct by metadata.Container is null.
    }
}

As you see from the code comments, inside OnMetadataCreated I need to access the Model class instance but, though ContainerType is correct, the Container property is NULL.

Can you please help me by giving me a hint regarding this issue?

THANK YOU IN ADVANCE!

Evdin

LATER EDIT

Considering that I haven't gave to much explanations, I will also paste here an example on how I would like to use this attribute on a model class:

/// <summary>
/// Gets or sets the IsAccountCreated
/// </summary>
/// <value>The IsAccountCreated.</value>
[UIHint("FormFieldStringTemplate")]
[ExtendedAdditionalMetadata("IsExternalAccount", "true", "ReadOnly", "true")]
public override Boolean IsAccountCreated { get; set; }      

/// <summary>
/// Gets or sets the IsAccountEnabled
/// </summary>
/// <value>The IsAccountEnabled.</value>
[Display(Name = "Este cont activ?")]
[UIHint("FormFieldStringTemplate")]
[ExtendedAdditionalMetadata("IsExternalAccount", "true", "ReadOnly", "true")]
public override Boolean IsAccountEnabled { get; set; }      

/// <summary>
/// Gets or sets the IsExternalAccount
/// </summary>
/// <value>The IsExternalAccount.</value>
[Display(Name = "Este cont extern?")]
[UIHint("FormFieldStringTemplate")]
[AdditionalMetadata("ReadOnly", "true")]
public override Boolean IsExternalAccount { get; set; } 

Later & Later Edit

Though the response given by @stephen-muecke is more then simple and acceptable in current situation, for the sake of programming challenge I've looked for other options and I found the following possibility: implementing a custom DataAnnotationsModelMetadataProvider class. In few simple words - it works and I'm able to obtain the model class instance BUT only if the model class is a simple class, otherwise there are many drawbacks - for example if you have a Model class and you use it in your view then it's ok but if you have a class inside another class (a model inside a viewmodel) that this approach is not usable anymore.

Thank you again @stephen-muecke!

3
  • Try metadata.Model Commented Oct 21, 2014 at 8:45
  • It doesn't help because metadata.Model gives me the value of the property on which the attribute is applied on and I don't need this value - what I need to do is to add a value to the AdditionalValues dictionary if the value of the property taken from extraFieldToCheck equals the value of the property extraFieldValueToCheck. Commented Oct 21, 2014 at 8:54
  • So far I've tried the following approach - accessing the "other" property through reflection like this: PropertyInfo modelProperty = metadata.ContainerType.GetProperty(this.extraFieldToCheck, BindingFlags.Public|BindingFlags.NonPublic|BindingFlags.Static); but the modelProperty is always null. Commented Oct 21, 2014 at 8:56

1 Answer 1

0

Since you seem to need access to multiple properties of the model, the attribute should target class (AttributeTargets.Class) and be applied to the model, not a property. This might mean you need to add another property that is the name of the property you were trying to apply this to. Note metadata.ContainerType only gives you the type, not this instance so you can only get the default value of its properties.

Edit

If the attributes need to be applied to multiple properties in the model, then you cannot access the container in OnMetadataCreated because metadata is created from the innermost properties out so the model's metadata has not yet been created.

Based on OP's comments, a better solution would be to create a custom html helper. For example to generate a textbox that is readonly based on the value of another property

namespace MyHelpers.Html
{
  public static class ReadOnlyHelpers
  {
    public static MvcHtmlString ReadOnlyTextBoxIf<TModel, TValue>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression, bool isReadOnly)
    {
      object attributes = isReadOnly ? new { @readonly = "readonly" } : null;
      return InputExtensions.TextBoxFor(helper, expression, attributes);
    }
  }
}

and use in your view as

@Html.ReadOnlyTextBoxIf(m => m.SomeTextProperty, Model.SomeBooleanValue)

Creating a 'Readonly' checkbox is a little more difficult because the readonly attribute has no affect with a checkbox. In order to prevent user interaction you need to disable it but that means the value wont post back

public static MvcHtmlString ReadOnlyCheckBoxIf<TModel>(this HtmlHelper<TModel> helper, Expression<Func<TModel, bool>> expression, bool isReadOnly)
{
  if (isReadOnly)
  {
    // If you want to 'visually' render a checkbox (otherwise just render a div with "YES" or "NO")
    ModelMetadata metaData = ModelMetadata.FromLambdaExpression(expression, helper.ViewData);
    StringBuilder html = new StringBuilder();
    // Add a hidden input for postback
    html.Append(InputExtensions.HiddenFor(helper, expression).ToString());
    // Add a visual checkbox without name so it does not post back
    TagBuilder checkbox = new TagBuilder("input");
    checkbox.MergeAttribute("type", "checkbox");
    checkbox.MergeAttribute("disabled", "disabled");
    if ((bool)metaData.Model)
    {
      checkbox.MergeAttribute("checked", "checked");
    }
    html.Append(checkbox.ToString());
    return MvcHtmlString.Create(html.ToString());
  }
  else
  {
    // return normal checkbox
    return InputExtensions.CheckBoxFor(helper, expression);
  }
}

and use in your view as

@Html.ReadOnlyCheckBoxIf(m => m.IsAccountCreated, Model.IsExternalAccount)
Sign up to request clarification or add additional context in comments.

12 Comments

First of all - this will solve (theoretically) only the problem of getting the instance of the class, but on the other hand it will make the code a bit uglier (please, excuse me but this is my personal opinion) because, if (again, theoretically) I will have a model with 25 properties and I need to put this attribute on 10 of them, it will get very very ugly... Second of all, I've tried your approach, and though I've declared the attribute scope to be class and I decorated the model class with this attribute, the OnMetadataCreated method doesn't get called... Don't know why... :(
I agree that if its applicable to more than one property this is not the right solution. As for not working, not sure why - it certainly works for me.
Just to give some extra explanation, metadata is created from inner to outer so the metadata for the model (class) is created last. You haven't explained the context for this but I assume you have a custom html helper. If that's the case then you add the values to the dictionary (metadata.AdditionalValues["SomeKey"] = extraFieldToCheck), and in the helper where you have access to the full metatdata, you can then get access to those keys via metadata.Properties
Yes - I think you're right - I haven't given to much explanations about my goal... In few words, a real example: I have a table which contains a field for example - IsExternalAccount; depending on this field value, I need to add to the AdditionalValues dictionary a value called let's say ReadOnly for other fields; for example: [ExtendedAdditionalMetadata("IsExternalAccount", "true", "ReadOnly", "true")] public override Boolean IsAccountCreated { get; set; }. What I'm trying to achieve is to add (based on a condition) a value to a dictionary. That's all...
I think you are going about this the wrong way and overly complicating the problem. A custom helper, say, @Html.ReadOnlyIfFor(m => m.IsAccountCreated, Model.IsExternalAccount) that renders a readonly control for IsAccountCreated if IsExternalAccount is true (and editable if false) would be much simpler and more reusable. Its late, but if you need further help, I'll have a look in the morning.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.