1

In my .NET MAUI app, I have a feed that displays some data along with a like button. Because I have different variations of this feed in the same app, I created a ContentView with BindableProperty's so that I can reuse my code.

Initially, things appear to work fine but when user taps the like button, even though the request gets processed correctly in parent view model method, it doesn't invoke a UI update.

If I simply copy and paste the same XAML code into the ContentPage and NOT use a ContentView, everything works fine and any change to IsLiked property triggers a UI update. Everything seems to be wired correctly with the ContentView. For example, tapping the button, does call the LikeButtonTapped in parent page's view model. Any idea what maybe causing this?

For brevity, I simplified things a bit here. Here's the FeedItemComponent.xaml card:

<ContentView...>
   <Grid
      RowDefinitions="20,*,30">

      <Label
         Grid.Row="0"
         x:Name="ItemTitle"
         Color="Black" />

      <Label
         Grid.Row="1"
         x:Name="ItemBody"
         Color="Black" />

      <Button
         Grid.Row="2"
         x:Name="LikeButton" />
      <Image
         Grid.Row="2"
         x:Name="LikedImage"
         Source="heart.png" />
   </Grid>
</ContentView>

Here's the code behind for FeedItemComponent.xaml.cs i.e. FeedItemComponent:

public partial class FeedItemComponent : ContentView
{
   public static readonly BindableProperty FeedItemContentProperty =
        BindableProperty.Create(nameof(FeedItemContent),
            typeof(FeedItemModel),
            typeof(FeedItemComponent),
            defaultValue: null,
            defaultBindingMode: BindingMode.OneWay,
            propertyChanged: OnFeedItemContentPropertyChanged);

    public static readonly BindableProperty LikeCommandProperty =
        BindableProperty.Create(nameof(LikeCommand),
            typeof(ICommand),
            typeof(FeedItemComponent));

    private static void OnFeedItemContentPropertyChanged(BindableObject bindable, object oldValue, object newValue)
    {
        var control = (FeedItemComponent)bindable;
        var feedItemContent = (FeedItemModel)newValue;
        control.ItemTitle.Text = feedItemContent.Title;
        control.ItemBody.Text = feedItemContent.Body;
        control.LikeButton.Command = control.LikeCommand;
        control.LikeButton.CommandParameter = feedItemContent;
        control.LikeButton.IsVisible = feedItemContent.IsLiked ? false : true;
        control.LikedImage.IsVisible = feedItemContent.IsLiked ? true : false;
    }

    public FeedItemModel FeedItemContent
    {
        get => (FeedItemModel)GetValue(FeedItemContentProperty);
        set => SetValue(FeedItemContentProperty, value);
    }

    public ICommand LikeCommand
    {
        get => (ICommand)GetValue(LikeCommandProperty);
        set => SetValue(LikeCommandProperty, value);
    }

    public FeedItemComponent()
    {
        InitializeComponent();
    }
}

Here's the FeedItem model:

public partial class FeedItemModel : ObservableObject
{
   public Guid Id { get; set; }

   public string Title { get; set; }

   public string Body { get; set; }

   [ObservableProperty]
   bool isLiked;
}

Here's the view model (MainPageViewModel.cs) for the MainPage:

public partial class MainPageViewModel : ObservableObject
{
   public ObservableCollection<FeedItemModel> Feed { get; } = new();

   [RelayCommand]
   async Task LikeButtonTapped(FeedItemModel feedItem)
   {
      // Make API call
      await _myApiService.FeedItemLiked(feedItem.Id, userId);

      // Update feed item "Liked" status
      foreach(var item in Feed)
         if(item.Id == feedItem.Id)
            item.IsLiked = true;
   }

   internal async Task Init()
   {
      // Initialize and fetch initial feed data
      var data = await _myApiService.GetData();
      if(data != null && data. Count > 0)
         foreach(var item in data)
            Feed.Add(item);
   }
}

And finally, here's the XAML for MainPage.xaml:

<ContentPage...>
   <ContentPage.Resources>
      <ResourceDictionary>
         <DataTemplate
            x:Key="FeedItemTemplate"
            x:DataType="model:FeedItemModel">
               <controls:FeedItemComponent
                  FeedItemContent="{Binding .}"
                  LikeCommand="{Binding Source={x:Reference Name=Feed}, Path=BindingContext.LikeButtonTappedCommand}" />
         </DataTemplate>
      </ResourceDictionary>
   </ContentPage.Resources>
   <CollectionView
      x:Name="Feed"
      ItemsSource="{Binding Feed}"
      ItemTemplate="{StaticResource FeedItemTemplate}">
   </CollectionView>
</ContentPage>

Any idea why updates to items of the ObservableCollection for Feed don't trigger a UI update IF I use ContentView component?

Sam
  • 26,817
  • 58
  • 206
  • 383
  • You are manually setting the UI properties instead of using data binding – Jason Jul 20 '23 at 05:18
  • Could you please point me to some sample code? I’ve just started with `BindableProperty` in `ContentView`‘s so I’m still learning how to use them. Thanks. – Sam Jul 20 '23 at 05:29
  • Literally what you are doing in your previous post. `Text=“{Binding SomeProperty}”` – Jason Jul 20 '23 at 05:37
  • https://learn.microsoft.com/en-us/dotnet/maui/fundamentals/data-binding/ – Jason Jul 20 '23 at 05:38
  • The knowledge you are missing, is how to data bind to properties of a custom component, since the component needs to inherit BindingContext from its parent. I’ve added an answer to clarify for your case. And linked to my main answer on this topic. – ToolmakerSteve Jul 20 '23 at 06:39

1 Answers1

1

The knowledge you are missing, is how to data bind to properties of a custom component, since the component needs to inherit BindingContext from its parent (so you have to do something special when Binding).

<ContentView x:Name="me" ..>

  <Button x:Name="LikeButton"
    IsVisible="{Binding FeedItemContent.IsLiked, Source={x:Reference me}}" />
  • Use the inverse boolean value converter, where needed. I left that out above.
ToolmakerSteve
  • 18,547
  • 14
  • 94
  • 196
  • 1
    Thank you for the detailed explanation. I updated my code and it works like a charm! – Sam Jul 20 '23 at 16:12