neggenbe
neggenbe

Reputation: 1885

Blazor : is it possible to bind a generic item to a property by its name using reflection?

I'm trying to get a generic editor template using Blazor that binds to properties found by reflection as follows :

@typeparam TItem

@foreach (var propertyName in FieldsList) {
    <div>
        <InputText id="name" @bind-Value="@getBindProperty(propertyName)" />
    </div>
}

@functions {
    [Parameter]
    public TItem ItemEditModel { get; set; }

    public string[] FieldsList {
        get {
            // get all properties decorated with some custom attribute...
            return typeof(TItem).GetProperties().Where(prop => Attribute.IsDefined(prop, typeof(MyCustomAttribute))).Select(x => x.Name).ToArray();
        }
    }

    public string getBindProperty(string propName) {
        return string.Format("{0}.{1}", nameof(ItemEditModel), propName);
    }
}

The above is not accepted as The left-hand side of an assignment must be a variable, property or indexer.

So I cannot bind to a property by its name, so I went on trying other syntax like

<InputText id="name" @bind-Value="ItemEditModel.propertyName" />

This is not accepted either as TItem does not contain a definition of 'propertyName'.

Well all the above make sense - but the key question - is there anything to be done about it, or is it impossible to bind a control to a property by its name?

BONUS QUESTION: If it is actually possible to do this, is there a way to make a switch depending on the property type (typically, only primitive types like ´string´, ´DateTime´, ´int´, ´double´, ´bool´...)?

Upvotes: 2

Views: 4225

Answers (2)

copycat_am
copycat_am

Reputation: 233

If you use InputText Blazor's component you should wrap it with the EditForm Blazor's component.

An alternative way to do it could be probably as follows:

    @typeparam TItem

    @foreach (var propertyName in FieldsList)
    {
       <div>
          <input @bind="PropertyVars[propertyName]" />
       </div>
    }

    <button type="button" @onclick="OnSubmit">Reset</button>

    @code {
        [Parameter]
        public TItem ItemEditModel { get; set; }

       public Dictionary<string, string> PropertyVars { get; set; } = new 
     Dictionary<string, string>();

   protected override void OnInitialized()
   {
     foreach (var propertyName in FieldsList)
     {
        var propertyInfo = ItemEditModel.GetType().GetProperty(propertyName);
        PropertyVars.Add(propertyName, 
        propertyInfo?.GetValue(ItemEditModel).ToString());
     }

  }

  public string[] FieldsList
  {
    get
    {
        return typeof(TItem).GetProperties().Select(x => x.Name).ToArray();
    }
  }

  private void OnSubmit()
  {
    var tt = PropertyVars;

    foreach (var propertyName in FieldsList)
    {
        var propertyInfo = ItemEditModel.GetType().GetProperty(propertyName);

        var uu = PropertyVars[propertyName].GetType();

        if (uu == propertyInfo?.PropertyType)
        {
            propertyInfo.SetValue(ItemEditModel, 
            Convert.ChangeType(PropertyVars[propertyName], propertyInfo.PropertyType),
            null);
        }

    }

    var yy = ItemEditModel;
 }
}

Upvotes: 4

neggenbe
neggenbe

Reputation: 1885

To wrap up the solution I ended up with:

  1. I created indexed property class wrappers that allow me to access values (note that I created 2 additional ones that are readonly and writeonly, in case it helps)

     public class IndexedProperty<TIndex, TValue> {
         readonly Action<TIndex, TValue> SetAction;
         readonly Func<TIndex, TValue> GetFunc;
    
         public IndexedProperty(Func<TIndex, TValue> getFunc, Action<TIndex, TValue> setAction) {
           this.GetFunc = getFunc;
           this.SetAction = setAction;
         }
         public TValue this[TIndex i] {
           get {
             return GetFunc(i);
           }
           set {
             SetAction(i, value);
           }
         }
       }
    
    
       public class ReadOnlyIndexedProperty<TIndex, TValue> {
             readonly Func<TIndex, TValue> GetFunc;
    
         public ReadOnlyIndexedProperty(Func<TIndex, TValue> getFunc) {
           this.GetFunc = getFunc;
         }
    
         public TValue this[TIndex i] {
           get {
             return GetFunc(i);
           }
         }
       }
    
    
       public class WriteOnlyIndexedProperty<TIndex, TValue> {
         readonly Action<TIndex, TValue> SetAction;
    
         public WriteOnlyIndexedProperty(Action<TIndex, TValue> setAction) {
           this.SetAction = setAction;
         }
    
         public TValue this[TIndex i] {
           set {
             SetAction(i, value);
           }
         }
       }
    
  2. I created a "BindableBase" object that all my objects inherit from, in wich I added the index property :

    IndexedProperty<string, string> _strValues;
    [JsonIgnore]
    public IndexedProperty<string, string> StrValues {
      get {
        if (_strValues == null) {
          _strValues = new IndexedProperty<string, string>(GetStrValue, SetStrValue);
        }
        return _strValues;
      }
    }
    
    private string GetStrValue(string propName) {
      // use reflection here to get the property value as string
      // --- NOTE: beware of performance here I suggest you take a look at related link
    }
    
    private void SetStrValue(string propName, string value) {
      // use reflection here to SET the property value from string
      // --- NOTE: beware of performance here I suggest you take a look at related link
    }
    

Check this link if performance is of matter to you!

  1. In my form, I bind using the indexed properties as follows :

    @foreach (var propertyName in FieldsList) {
        <div>
            <InputText id="name" @bind-Value="@myContextObject.StrValues[propertyName]" />
        </div>
    }
    

Credits to @copycar_am and this link.

Upvotes: 1

Related Questions