Shimmy Weitzhandler
Shimmy Weitzhandler

Reputation: 104841

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" />

Upvotes: 0

Views: 1873

Answers (2)

Shimmy Weitzhandler
Shimmy Weitzhandler

Reputation: 104841

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}}"/>

Upvotes: 0

DavidS
DavidS

Reputation: 2944

The best option is to define a value converter.

namespace SampleFormsApp {
public class DisplayNameConverter : IValueConverter
{
    public object Convert(object value, Type targetType,
      object parameter, CultureInfo culture)
    {
        if (value == null || targetType != typeof(string))
            return null;

        var propertyName = parameter as string;
        if (propertyName == null)
            return null;

        var propertyInfo = value.GetType().GetTypeInfo().DeclaredMembers
          .SingleOrDefault(m => m.Name == propertyName);
        if (propertyInfo == null)
            return null;

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

    public object ConvertBack(object value, Type targetType,
      object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Then declare it in your global ResourceDictionary in App.xaml:

<ResourceDictionary>
    <local:DisplayNameConverter x:Key="DisplayNameConverter"/>
</ResourceDictionary>

Making sure to declare the namespace:

xmlns:local="clr-namespace:SampleFormsApp"

Then when you want to use it, you bind to the object containing the property, and pass the property name as a parameter:

<Label Text="{Binding ., 
  Converter={StaticResource DisplayNameConverter}, ConverterParameter=FirstName}"/>

If you throw and exception in the Convert method (as your example above), it will crash the app. During page rendering, it will likely call the converter with a null value, so it has to be resilient to that at least.

Upvotes: 2

Related Questions