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!

Edi
  • 660
  • 9
  • 22
  • Try `metadata.Model` – artm Oct 21 '14 at 08: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`. – Edi Oct 21 '14 at 08: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. – Edi Oct 21 '14 at 08:56

1 Answers1

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)
  • 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... :( – Edi Oct 21 '14 at 09:26
  • 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. –  Oct 21 '14 at 09:32
  • 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 –  Oct 21 '14 at 10:13
  • 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... – Edi Oct 21 '14 at 12:00
  • I added an example on how I would like to use the attribute as a later edit to the initial question. Thanks! – Edi Oct 21 '14 at 12:06
  • So when applied to `bool IsAccountCreated` and if the value of `bool IsExternalAccount` is `true` what is the outcome or desired effect? –  Oct 21 '14 at 12:13
  • If applied to `IsAccountCreated` and `IsExternalAccount` is `true`, then `metadata.AdditionalValues["ReadOnly"] = true;` (you will find the `ReadOnly` and `true` as 3rd and 4th element of the attribute constructor. Thanks. – Edi Oct 21 '14 at 12:16
  • I understand that, but if your adding additional metadata then you must need to use it somehow/somewhere. What are you actually doing with the values - for example, are you trying to disable the checkbox for the `IsAccountCreated` property if `IsExternalAccount` is `true` –  Oct 21 '14 at 12:25
  • Yes, something like this - in an EditorTemplate I'm adding a class to make it readonly. – Edi Oct 21 '14 at 12:29
  • 1
    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. –  Oct 21 '14 at 12:40
  • Thank you for your answer. I'm thinking also that I'm over complicating myself... I'm taking one more shot and I will post the result pretty soon. Thanks again! – Edi Oct 21 '14 at 12:45
  • I'll update my answer shortly with a better solution (if I'm understanding the issue correctly) –  Oct 22 '14 at 00:13