Home > database >  Caliburn Micro MVVM : How to subscribe and unsubscribe event in ViewModel?
Caliburn Micro MVVM : How to subscribe and unsubscribe event in ViewModel?

Time:10-04

I created a WPF sample (using caliburn micro with MVVM pattern, no code-behind) with a view model and their related views:

  • ShellView.xaml and ShellViewModel.cs

The ShellView contains:

  • A ComobBox, which contains a list of string, if this combox selection is changed, it will raise comboBox1_SelectionChanged() in ShellViewModel.
  • A Button, if click this button, it will raise Button1_Click() to delete the first item of list in ShellViewModel.

My questions:

  • If I want to click the button without trigger comboBox1_SelectionChanged in view model, how to do that? If it implemented in code-behind, I can do like this:
        public void Button1_Click(object sender, EventArgs e)
        {
            comboBox1.SelectionChanged -= comboBox1_SelectionChanged;
            MyCollection.RemoveAt(0);
            comboBox1.SelectionChanged  = comboBox1_SelectionChanged;
        } 

I have no idea how to achieve this in view model. The following is the code:

ShellView.xaml

<UserControl x:Class="WpfApp.Views.ShellView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:WpfApp.Views"
             xmlns:cal="http://caliburnmicro.com" 
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height=" auto"/>
            <RowDefinition Height=" auto"/>
        </Grid.RowDefinitions>
        <ComboBox Name="comboBox1" Grid.Row="0" ItemsSource="{Binding MyCollection}" SelectedValue="{Binding SelectMyListValue}" 
                  cal:Message.Attach="[Event SelectionChanged]=[Action comboBox1_SelectionChanged($source,$eventArgs)]" />
        <Button Name="Button1" Grid.Row="1" Content="Delete" 
                cal:Message.Attach="[Event Click]=[Action Button1_Click($source,$eventArgs)]" />
    </Grid>
</UserControl>

ShellViewModel.cs

using Caliburn.Micro;
using System;
using System.Windows.Controls;

namespace WpfApp.ViewModels
{
    public class ShellViewModel : Conductor<object>.Collection.OneActive
    {
        private BindableCollection<string> _myCollection = new BindableCollection<string>() { "item1", "item2"};
        public BindableCollection<string> MyCollection
        {
            get => _myCollection;
            set
            {
                _myCollection = value;
                NotifyOfPropertyChange(() => MyCollection);
            }
        }

        private string _selectMyListValue = "item1";
        public string SelectMyListValue
        {
            get => _selectMyListValue;
            set
            {
                _selectMyListValue = value;
                NotifyOfPropertyChange(nameof(SelectMyListValue));
            }
        }


        public void comboBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            // Do something...
        }

        public void Button1_Click(object sender, EventArgs e)
        {
            MyCollection.RemoveAt(0);
        }
    }
}

Thank you in advance.

CodePudding user response:

Your requirement can't be fully met, as when you remove the selected item from the collection a change of SelectedValue (to null) is inevitable.

Furthermore: You don't need to bind to the SelectionChanged event. You already have a binding to SelectedValue, so the setter of the bound property is called when the selection changes. This doesn't happen, when you remove a value from the collection that is not currently selected.

I would also recommend not to subscribe to the Clicked event of the button, but to bind an ICommand (added to your viewmodel) to the Command property of the button. An easy to use implementation would be the RelayCommand from the Windows Community Toolkit. You can read about it here: https://learn.microsoft.com/en-us/windows/communitytoolkit/mvvm/relaycommand. It also isn't difficult to implemnt a version on your own, if you don't want to use the whole toolkit.

Code sample:

public class RelayCommand : ICommand
{
    private readonly Action<object?> execute;

    private readonly Func<object?, bool> canExecute;

    public RelayCommand(
        Action<object?> execute,
        Func<object?, bool>? canExecute = null)
    {
        this.execute = execute;
        this.canExecute = canExecute ?? (_ => true);
    }

    public bool CanExecute(object? parameter) => this.canExecute(parameter);

    public void Execute(object? parameter)
    {
        this.execute(parameter);
    }

    public event EventHandler? CanExecuteChanged;
}

// on your viewmodel add...
public ICommand RemoveFirstItemCommand { get; set; }

private void RemoveFirstItem(object? param)
{
    if (this.Items.Count > 0)
    {
        this.Items.RemoveAt(0);
    }
}

// ...and in the constructor init the command
this.RemoveFirstItemCommand = new RelayCommand(this.RemoveFirstItem);

CodePudding user response:

I got a solution which achieved the goal, but I'm not sure if it's the right way.

There is a "Microsoft.Xaml.Behaviors" which provided "Interaction.Triggers" that contains "ComparisonCondition". I can use it to bind a value to determine the EventCommand is raised or not.

I updated the code as following:

ShellViewModel.cs

using Caliburn.Micro;
using System;
using System.Windows.Controls;
using WpfApp.Commands;

namespace WpfApp.ViewModels
{
    public class ShellViewModel : Conductor<object>.Collection.OneActive
    {
        private bool _IsEnableSelectionChangedCommand = true;
        public bool IsEnableSelectionChangedCommand
        {
            get => _IsEnableSelectionChangedCommand;
            set
            {
                _IsEnableSelectionChangedCommand = value;
                NotifyOfPropertyChange(() => IsEnableSelectionChangedCommand);
            }
        }

        private BindableCollection<string> _myCollection = new BindableCollection<string>() { "item1", "item2"};
        public BindableCollection<string> MyCollection
        {
            get => _myCollection;
            set
            {
                _myCollection = value;
                NotifyOfPropertyChange(() => MyCollection);
            }
        }

        private string _selectMyListValue = "item1";

        public DelegateCommand<object> DoSelectionChangedCommand { get; }

        public ShellViewModel()
        {
            DoSelectionChangedCommand = new DelegateCommand<object>(comboBox1_SelectionChanged, CanExecute);
        }

        private bool CanExecute(object param)
        {
            return true;
        }

        private void comboBox1_SelectionChanged(object param)
        {
            SelectionChangedEventArgs e = param as SelectionChangedEventArgs;
            ComboBox item = e.Source as ComboBox; 

            // Do something...

        }

        public string SelectMyListValue
        {
            get => _selectMyListValue;
            set
            {
                _selectMyListValue = value;
                NotifyOfPropertyChange(nameof(SelectMyListValue));
            }
        }

        public void Button1_Click(object sender, EventArgs e)
        {
            IsEnableSelectionChangedCommand = false;
            MyCollection.RemoveAt(0);
            IsEnableSelectionChangedCommand = true;
        }
    }
}

ShellView.xaml

<UserControl x:Class="WpfApp.Views.ShellView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:cal="http://caliburnmicro.com" 
             xmlns:i="http://schemas.microsoft.com/xaml/behaviors" 
             xmlns:cmd="clr-namespace:WpfApp.Commands"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height=" auto"/>
            <RowDefinition Height=" auto"/>
        </Grid.RowDefinitions>
        <ComboBox Name="comboBox1" Grid.Row="0"  ItemsSource="{Binding MyCollection}" SelectedValue="{Binding SelectMyListValue}">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="SelectionChanged">
                    <cmd:EventCommand Command="{Binding DoSelectionChangedCommand}" />
                    <i:Interaction.Behaviors>
                        <i:ConditionBehavior>
                            <i:ConditionalExpression>
                                <i:ComparisonCondition LeftOperand= "{Binding IsEnableSelectionChangedCommand}" Operator="Equal"  RightOperand="True"/>
                            </i:ConditionalExpression>
                        </i:ConditionBehavior>
                    </i:Interaction.Behaviors>
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </ComboBox>
        <Button Name="Button1" Grid.Row="1" Content="Delete" 
                cal:Message.Attach="[Event Click]=[Action Button1_Click($source,$eventArgs)]" />
    </Grid>
</UserControl>
  • Related