Reputation: 35
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
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
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
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
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
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 :
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);
}
}
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