Why does this Binding not work, when the property is referenced in a ObservableCollection?

Viewed 340

I have the following problem, which I cannot comprehend. I'm quite new to Xamarin, and I've been working on this particular problem for quite some time now.

TL;DR: I have an MyGroups[] array, which is used by a Repeater in XAML. The DataTemplate, used by the Repeater, has an ObservableCollection, which is populated in XAML. Binding one of MyGroups's properties with Description.Title does not work, however using the same property outside of the Description Collection works just fine.

Long story:

Let's say we have a Repeater, which is populated by the VM's MyGroups[] property. The Repeater and Models:Group look something like this:

<Grial:Repeater ItemTemplate = "{ DynamicResource GroupDataTemplate }"
                           ItemsSource  = "{ Binding MyGroups }"/>
namespace MyApp.Models
{
   public class Group
   {
      public string MyProperty
      {
         get;
         set;
      }

      public string MyOtherProperty
      {
         get;
         set;
      }
   }
}

This Repeater gets the following DataTemplate:

<DataTemplate x:Key = "GroupDataTemplate">
   <Cards:CardTemplate x:DataType = "Models:Group">

      <StackLayout HorizontalOptions = "Center"
                   VerticalOptions   = "Center">
         <Label>
            <Label.Text>
               <MultiBinding StringFormat="{} {0} {1}">     //<---- this works
                  <Binding Path = "MyProperty" />
                  <Binding Source = "{ x:Static Strings:AppResources.MyString }"/>
               </MultiBinding>
            </Label.Text
         </Label>
      </StackLayout>


      <Cards:CardTemplate.Descriptions>
         <Models:Description>
            <Models:Description.Title>
               <MultiBinding StringFormat="{} {0} {1}">     //<---- this value is always null
                  <Binding Path = "MyProperty" />
                  <Binding Source = "{ x:Static Strings:AppResources.MyString }"/>
               </MultiBinding>
            </Models:Description.Title>
         </Models:Description>


         <Models:Description>
            <Models:Description.Title>
               <MultiBinding StringFormat="{} {0} {1}">     //<---- this value is always null
                  <Binding Path = "MyOtherProperty" />
                  <Binding Source = "{ x:Static Strings:AppResources.MyString }"/>
               </MultiBinding>
            </Models:Description.Title>
         </Models:Description>

      </Cards:CardTemplate.Descriptions>
   </Cards:CardTemplate>
</DataTemplate>

The Cards:CardTemplate looks something like this:

<Grial:CardView ...>

   ...

   <Grial:GridView RowSpacing  = "16"
                   Grid.Row    = "1"
                   ColumnCount = "2"
                   ItemsSource = "{ Binding Source={ RelativeSource AncestorType={ x:Type Cards:CardTemplate } }, Path=Descriptions }">

      <Grial:GridView.ItemTemplate>
         <DataTemplate x:Name = "x_cardItemsDescriptionTemplate">
            <StackLayout Spacing     = "5"
                         Orientation = "Horizontal">

               <Label VerticalOptions = "Center"
                      Text            = "{ Binding Source={ RelativeSource AncestorType={ x:Type Models:Description } }, Path=Icon }"/>

               <Label VerticalOptions = "Center"
                      Text            = "{ Binding Source={ RelativeSource AncestorType={ x:Type Models:Description } }, Path=Description }"/>
            </StackLayout>
         </DataTemplate>
      </Grial:GridView.ItemTemplate>
   </Grial:GridView>
</Grial:CardView>

The Description looks like this

using System;

using Xamarin.Forms;


public class Description
   : BindableObject
{
   public static readonly BindableProperty IconProperty = BindableProperty.Create( propertyName: nameof( Description.Icon ),
                                                                                   returnType: typeof( string ),
                                                                                   declaringType: typeof( Description ),
                                                                                   defaultBindingMode: BindingMode.TwoWay,
                                                                                   propertyChanged: Description.OnIconChanged );

   public static readonly BindableProperty TitleProperty = BindableProperty.Create( propertyName: nameof( Description.Title ),
                                                                                    returnType: typeof( string ),
                                                                                    declaringType: typeof( Description ),
                                                                                    defaultBindingMode: BindingMode.TwoWay,
                                                                                    propertyChanged: Description.OnTitleChanged );

   public string Icon
   {
      get => (string)GetValue( IconProperty );
      set => SetValue( property: IconProperty, value: value );
   }

   public string Title
   {
      get => (string)GetValue( TitleProperty );
      set => SetValue( property: TitleProperty, value: value );
   }


   private static void OnIconChanged( BindableObject bindable, object old_value, object new_value )
   {
      try
      {
         ( (Description)bindable ).Icon = new_value as string;
      }
      catch( Exception e )
      {
         ...
      }
   }

   private static void OnTitleChanged( BindableObject bindable, object old_value, object new_value )
   {
      try
      {
         ( (Description)bindable ).Title = new_value as string;
      }
      catch( Exception e )
      {
         ...
      }
   }
}

What I don't understand is why {Binding Path=MyProperty} works for the Label, but not for the Description. The problem is not the CardTemplate, because every hard-coded or resource-bound string gets assigned to the appropriate element. The only problem is the binding itself!

Things I've tried:

  • BindingList instead of ObservableCollections
  • Description inherits from INotifyPropertyChanged/BindableObject
  • Assigning "Source={ RelativeSource AncestorType={ x:Type Models:Group } }" with or without levels
  • {Binding .MyProperty}, {Binding Models:Groups.MyProperty}, {Binding /MyProperty}
  • Implementing INotifyPropertyChanged in Group

Nothing seems to work, and I cannot understand why? Why does the Label.Text has its value set correctly, but Description.Title not. Is there something that I'm missing?

1 Answers

I have found a solution/workaround to my problem. It's not 100% how I wanted it, but, meh, close enough I guess.

What I couldn't achieve is to use MultiBindings, but I don't strictly need to concatenate multiple strings in my use case, so I settled for a StringFormat.

<Cards:CardTemplate x:Name = "template">
    <Cards:CardTemplate.Descriptions>
        <Models:Description Title = "{ Binding Path=BindingContext.MyProperty, Source={ x:Reference Name=template }, StringFormat=... }"/>
    </Cards:CardTemplate.Descriptions>
</Cards:CardTemplate>

The two important points are the BindingContext. of the Path=BindingContext.MyProperty and the Source referencing the container View. Since the MyProperty is only available on the level of the CardTemplate, all children of the CardTemplate have no idea of its existence. To make if available to them, we use the Source property to reference the template, and then use BindingContext to reference the current object of the ViewModel's container (in this case MyGroup). Since we now have access to each MyGroup object, we can call its properties (in this case MyProperty), thus reaching our goal.

Why this does not work with MultiBinding is beyond me, but it's more than enough rn.

Related