Search code examples
c#wpfmvvmbindingmvvm-light

WPF command binding works only a few times


I have created a UserControl named AnswerUserControl. XAML file looks like this (it's only button):

<UserControl
    ...>

    <Button
        DataContext="{Binding Path=ViewModel, 
                      RelativeSource={
                          RelativeSource Mode=FindAncestor,
                          AncestorType={x:Type UserControl}
                      }}"
        Command="{Binding Path=ClickCommand}"
        Content="{Binding Path=Reply}" />

</UserControl>

And cs file with dependency property to AnswerViewModel:

public partial class AnswerUserControl : UserControl
{
    public AnswerUserControl()
    {
        InitializeComponent();
    }

    public AnswerViewModel ViewModel
    {
        get
        {
            return (AnswerViewModel)GetValue(ViewModelProperty);
        }
        set
        {
            SetValue(ViewModelProperty, value);
        }
    }

    public static readonly DependencyProperty ViewModelProperty =
        DependencyProperty.Register("ViewModel", typeof(AnswerViewModel), typeof(AnswerUserControl));
}

AnswerViewModel class:

public class AnswerViewModel : ViewModelBase
{
    private string reply;

    public string Reply
    {
        get
        {
            return reply;
        }
        set
        {
            reply = value;
            RaisePropertyChanged(nameof(Reply));
        }
    }

    private ICommand clickCommand;

    public ICommand ClickCommand
    {
        get
        {
            return clickCommand;
        }
        set
        {
            clickCommand = value;
            RaisePropertyChanged(nameof(ClickCommand));
        }
    }

    public AnswerViewModel(Action<int> click, string reply, int index)
    {
        void Click()
        {
            Console.WriteLine($"AnswerViewModel click at {index}");
            click(index);
        }

        Reply = reply;
        ClickCommand = new RelayCommand(Click);
    }
}

In Window I am adding this UserContol like this:

<StackPanel>
    <local:AnswerUserControl
        ViewModel="{Binding Path=VMA}" />
</StackPanel>

VMA is an AnswerViewModel in Window's ViewModel:

private AnswerViewModel vma;

public AnswerViewModel VMA
{
    get
    {
        return vma;
    }
    set
    {
        vma = value;
        RaisePropertyChanged(nameof(VMA));
    }
}

static int number = 1;

public HomeViewModel()
{
    VMA = new AnswerViewModel(x => { Console.WriteLine($"Click Number: {number++}"); }, "Reply", 0);
}

The problem I am facing is that ClickCommand is executed only a few times. When I click slowly on the button it is executed from 4 to 6 times and when I click fast it is executed over 20 times and then it stops working. Where I am making a mistake?


Solution

  • I wrote a crude version of how it can be implemented. For the mainwindow we can use the following code.

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new MainWindowDataContext();
        }
    }
    

    As seen above we assign a viewmodel to the mainwindow, in this viewmodel we can define a datacontext for the usercontrol. Giving us the following code;

    public class MainWindowDataContext : INotifyPropertyChanged
    {
        public MainWindowDataContext()
        {
            newDatacontext = new DataContextTest();
    
        }
        private DataContextTest _newDatacontext;
    
        public DataContextTest newDatacontext
        {
            get => _newDatacontext;
            set
            {
                if(_newDatacontext == value)
                    return;
                _newDatacontext = value;
                OnPropertyChanged(nameof(newDatacontext));
            }
        }
    
        public event PropertyChangedEventHandler PropertyChanged;
    
        protected void OnPropertyChanged([CallerMemberName] string name = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
    }
    

    Do not mind the interface or the getter/setter, this is purely for demonstrating how binding can be notified that a property has changed.

    Within the MainWindowDataContext we create a DataContextTest class. This viewmodel/datacontext class holds our command and can hold other bindings for the usercontrol.

    public class DataContextTest
    {
        public ICommand ButtonCommand => new RelayCommand(executeThis);
    
    
        private void executeThis()
        {
            Console.WriteLine($"VM clicked ");
        }
    }
    

    For the front side where the xaml code and bindings reside I wrote the following things; A usercontrol that has a button, and in the mainwindow I inserted the usercontrol.

    <Window x:Class="Tryout.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Tryout"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <local:UserControl1 DataContext="{Binding newDatacontext,UpdateSourceTrigger=PropertyChanged}"></local:UserControl1>
    </Grid>
    

    The mainwindow uses the MainWindowDataContext, this is being assigned in the constructor (see code block 1). The MainWindowDataContext has a getter setter for the usercontrol that needs a datacontext. The userControl gets notified when the datacontext changes.

    When looking at the usercontrol xaml we see the following

    <UserControl x:Class="Tryout.UserControl1"
             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:Tryout"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <Button Command="{Binding ButtonCommand}" Width="200" Height="100" Content="test"/>
    </Grid>
    

    With all this code it resolves to the following hierarchy for the viewmodels.

    MainWindow -> MainWindowDataContext -> DataContextTest.

    Because the MainWindow uses the UserControl we need to define a viewmodel for the MainWindow. Once that is set you can assign a datacontext/viewmodel to the usercontrol through binding on the front end.

    No need for Dependecy Properties, a datacontext would suffice in most of the cases (: