Natash Dimas
Natash Dimas

Reputation: 35

Binding a relay command from a class to a CustomCommand mismatching

I am making a ribbon like menu.

For achieving, I created three basic components.

The class define each item

public class MenuComponentItems {

    public string ImageSource { get; set; }

    public string CommandParameter { get; set; }

    public string Section { get; set; }

    public IRelayCommand Command { get; set; }

}

then we will define the UI

<Border
    Margin="5,5,0,0"
    HeightRequest="120"
    VerticalOptions="StartAndExpand">
    <Grid
        RowSpacing="5"
        VerticalOptions="Center">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <HorizontalStackLayout
            x:Name="MenuItemsStack"
            BackgroundColor="Bisque" />

        <Label
            Grid.Row="1"
            HorizontalTextAlignment="Center"
            Text="{Binding SectionName, Source={x:Reference MenuComponent}}" />
    </Grid>
</Border>

and finally, the code behind of the control

public partial class MenuTemplate : ContentView {

public ObservableCollection<MenuComponentItems> MenuItems { get; set; } = [];

public MenuTemplate() {
        InitializeComponent();
        MenuItems.CollectionChanged += (sender, e) => UpdateMenu();
}

public static readonly BindableProperty SectionNameProperty = BindableProperty.Create(
    nameof(SectionName),
    typeof(string),
    typeof(MenuTemplate));

public string SectionName {
    get => (string)GetValue(SectionNameProperty);
    set => SetValue(SectionNameProperty, value);
}

private void UpdateMenu() {
    MenuItemsStack.Children.Clear();

    foreach (var menuItem in MenuItems) {
        var imageButton = new ImageButton {
            Source = menuItem.ImageSource,
            WidthRequest = 60,
            HeightRequest = 60,
            Margin = new Thickness(10, 0, 10, 0),
            Command = menuItem.Command,
            CommandParameter = menuItem.CommandParameter
        };

        MenuItemsStack.Children.Add(imageButton);
    }
}

usage

<Grid RowDefinitions="200, *">
    <HorizontalStackLayout>
        <controls:MenuTemplate SectionName="ferwqfkrew">
            <controls:MenuTemplate.MenuItems>
                <models:MenuComponentItems
                    ImageSource="dotnet_bot.png"
                    Section="Section 1" />
                <models:MenuComponentItems
                    ImageSource="mario.png"
                    Section="Section 1" />
            </controls:MenuTemplate.MenuItems>
        </controls:MenuTemplate>
    </HorizontalStackLayout>
</Grid>

output

output

Problem

when I create a relay command in my view model

[RelayCommand]
public void MenuItemSelected(object param) {
    
    Debug.WriteLine("dcd");
}

and bind it I get

property, BindableProperty, or event found for "Command", or mismatching.

What I do not, is the fact that in my class I have a RelayCommand and my vm is a RelayCommand as well.

At first, I thought that is because a ImageButton command is an ICommand and not a RelayCommand, but I tested an ImageButton with the same command and it triggered

Thing that I tried

  1. Using ICommand instead of the RelayCommand of the Microsoft mvvm toolkit
  2. Created a RelayCommand property in the control itself

Update

I made my class inherit form ContentView

public class MenuComponentItems : ContentView {

    public string ImageSource { get; set; }

    public string CommandParameter { get; set; }


    public static readonly BindableProperty CommandProperty = BindableProperty.Create(
        nameof(Command), typeof(RelayCommand), typeof(MenuComponentItems));

    public RelayCommand Command {
        get => (RelayCommand)GetValue(CommandProperty);
        set => SetValue(CommandProperty, value);
    }
}

but it doesn't trigger the command.

Usage

public class MenuComponentItems : ContentView {

public string ImageSource { get; set; }

public string CommandParameter { get; set; }


public static readonly BindableProperty CommandProperty = BindableProperty.Create(
    nameof(Command), typeof(RelayCommand), typeof(MenuTemplate));

public RelayCommand Command {
    get => (RelayCommand)GetValue(CommandProperty);
    set => SetValue(CommandProperty, value);
}
<controls:MenuTemplate SectionName="ferwqfkrew">
    <controls:MenuTemplate.MenuItems>
        <models:MenuComponentItems Command="{Binding MenuItemSelectedCommand}"
            ImageSource="dotnet_bot.png" />
public partial class MainViewModel : ObservableObject {
     
    [RelayCommand]
    public void MenuItemSelected() {
    
        Debug.WriteLine("dcd");
    }
}

I inspected the MenuItemsStack.Children.Add(imageButton); and for some reason the command is null.

I tested it with a button, and it works, so the VM is fine, the problem is the custom control that doesn't get the command

Upvotes: 0

Views: 207

Answers (3)

Natash Dimas
Natash Dimas

Reputation: 35

I also found a solution.

private void UpdateMenu() {
    MenuItemsStack.Children.Clear();

    foreach (var menuItem in MenuItems) {
        var imageButton = new ImageButton {
            Source = menuItem.ImageSource,
            WidthRequest = 60,
            HeightRequest = 60,
        };

        imageButton.SetBinding(ImageButton.CommandProperty, new Binding("MenuItemSelectedCommand"));

        Binding parameter = new() {
        Source = imageButton
    };
    imageButton.SetBinding(ImageButton.CommandParameterProperty, parameter);

    MenuItemsStack.Children.Add(imageButton);
}

You just need to call relayCommand of your viewModel.

Upvotes: 0

Liqun Shen-MSFT
Liqun Shen-MSFT

Reputation: 7990

Yes I can also reproduce your issue. If you add breakpoints, you may find that UpdateMenu method in MenuTemplate is invoked before the value set to Command property. And when the Command property changes, the MenuItems.CollectionChanged event will not be invoked again. That's the reason why the Command property is always null.

So what I want to do here is to invoke UpdateMenu no matter when the Command property value changes.

To make it, I recommend you refer to this thread, ObservableCollection not noticing when Item in it changes (even with INotifyPropertyChanged). What you want is FullyObservableCollection<> instead of a ObservableCollection.

public partial class MenuTemplate : ContentView
{
    public FullyObservableCollection<MenuComponentItems> MenuItems { get; set; } = [];

    public MenuTemplate()
    {
        InitializeComponent();
        MenuItems.ItemPropertyChanged += (sender, e) => UpdateMenu();
    }
....

Using above code, no matter when the Commmand value changes, the UpdateMenu() will be invoked. The ImageButton will get the correct Command as well as CommandParameter.

Upvotes: 1

GChapX
GChapX

Reputation: 464

Okay, after trying several things, I actually found a workaround for your situation : it seems no Command was called because when generating your ImageButtons, the BindingContext of MenuComponentItems was null.

The workaround :

  • Set your UpdateMenu method to public and update BindingContext when iterating :
public void UpdateMenu() {
    MenuItemsStack.Children.Clear();

    foreach (var menuItem in MenuItems) {
        menuItem.BindingContext = this.BindingContext; // Somehow the MenuTemplate's BindingContext is correct, only item's one isn't set, we fix this here.
        var imageButton = new ImageButton {
            Source = menuItem.ImageSource,
            WidthRequest = 60,
            HeightRequest = 60,
            Command = menuItem.Command,
            CommandParameter = menuItem.CommandParameter
        };

        MenuItemsStack.Children.Add(imageButton);
    }
}
  • In your View, give a Name to your control, and after setting the BindingContext of the view and initializing the component, call UpdateMenu to force the Binding :
public MainPage(MainViewModel mainViewModel) {
    BindingContext = mainViewModel;
    InitializeComponent();
    YourMainTemplate.UpdateMenu();
}

Not gonna lie, I'm still not sure what the issue is and why this approach works, but at least now it calls your Command !

Upvotes: 1

Related Questions