Search code examples
c#reflectionbindingxamarin.formspropertyinfo

Xamarin.Forms access Binding object data


I want to make a label that will extract the name or some other data of the bound item.

[Display(Description = "Gimme your goddamm first name will ya")]
public string FirstName { get; set; }

Code:

public class TitleLabel : ContentView
{
  public Label Label { get; } = new Label();
  public TitleLabel()
  {
    //TODO ensure Content is not accessed manually
    Content = Label;
  }
  protected override void OnBindingContextChanged() =>
    Label.Text = GetPropertyTitle();


  string GetPropertyTitle()
  {
    var bcp = BindingContextProperty;

    //pseudo:
    var binding = GetBinding(bcp);
    var obj = binding.Object;
    var propertyName = binding.Path;
    var propertyInfo = obj.GetType().GetTypeInfo().DeclaredMembers
      .SingleOrDefault(m => m.Name == propertyName);
    if (propertyInfo == null)
      throw new InvalidOperationException();

    return propertyInfo.GetCustomAttribute<DisplayAttribute>().Description;
  }
}

XAML:

<my:TitleLabel Text="{Binding FirstName}" />

Rendered result:

<my:TitleLabel Text="Gimme your goddamm first name will ya" />

Solution

  • Gotcha (Gist):

    public class DisplayExtension : IMarkupExtension<string>
    {
      public object Target { get; set; }
      BindableProperty _Property;
    
      public string ProvideValue(IServiceProvider serviceProvider)
      {
        if (Target == null
          || !(Target is Enum
            || Target is Type
            || (Target is Binding binding && !string.IsNullOrWhiteSpace(binding.Path))))
          throw new InvalidOperationException($"'{nameof(Target)}' must be properly set.");
    
        var p =(IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
    
        if (!(p.TargetObject is BindableObject bo
          && p.TargetProperty is BindableProperty bp
          && bp.ReturnType.GetTypeInfo().IsAssignableFrom(typeof(string).GetTypeInfo())))
          throw new InvalidOperationException(
            $"'{nameof(DisplayExtension)}' cannot only be applied"
              + "to bindable string properties.");
    
        _Property = bp;
    
        bo.BindingContextChanged += DisplayExtension_BindingContextChanged;
        return null;
      }
    
      void DisplayExtension_BindingContextChanged(object sender, EventArgs e)
      {
        var bo = (BindableObject)sender;
        bo.BindingContextChanged -= DisplayExtension_BindingContextChanged;
    
        string display = null;
        if (Target is Binding binding)
          display = ExtractMember(bo, (Binding)Target);
        else if (Target is Type type)
          display = ExtractDescription(type.GetTypeInfo());
        else if (Target is Enum en)
        {
          var enumType = en.GetType();
          if (!Enum.IsDefined(enumType, en))
            throw new InvalidOperationException(
              $"The value '{en}' is not defined in '{enumType}'.");
          display = ExtractDescription(
            enumType.GetTypeInfo().GetDeclaredField(en.ToString()));
        }
        bo.SetValue(_Property, display);
      }
    
      string ExtractMember(BindableObject target, Binding binding)
      {
        var container = target.BindingContext;
        var properties = binding.Path.Split('.');
    
        var i = 0;
        do
        {
          var property = properties[i++];
          var type = container.GetType();
          var info = type.GetRuntimeProperty(property);
    
          if (properties.Length > i)
            container = info.GetValue(container);
          else
          {
            return ExtractDescription(info);
          }
        } while (true);
      }
    
      string ExtractDescription(MemberInfo info)
      {
        var display = info.GetCustomAttribute<DisplayAttribute>(true);
        if (display != null)
          return display.Name ?? display.Description;
    
        var description = info.GetCustomAttribute<DescriptionAttribute>(true);
        if (description != null)
          return description.Description;
    
        return info.Name;
      }
    
      object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider) =>
        ProvideValue(serviceProvider);
    }
    

    Usage:

    <Label Text="{my:Display Target={Binding FirstName}}"/>
    <Label Text="{my:Display Target={Binding User.Person.Address.City.Country}}"/>
    <Label Text="{my:Display Target={Type models:Person}}"/>
    <Label Text="{my:Display Target={Static models:Gender.Male}}"/>