簡體   English   中英

MVVM ICommand.CanExecute參數包含先前的值

[英]MVVM ICommand.CanExecute parameter contains previous value

我很難理解如果使用嵌套屬性而不是常規屬性,為什么ICommand.CanExecutes總是包含先前值而不是新值。

問題將在下面描述,除了使用某種形式的“門面”模式之外,我很難找到解決此問題的方法,在該模式下,我在viewmodel中創建屬性並將它們連接到模型中的相應屬性。

或使用該死的CommandManager.RequerySuggested事件。 之所以不理想,是因為該視圖僅顯示菜單就顯示了30多個命令,如果每次更改時所有CanExecute都進行更新,則所有菜單項/按鈕的更新將需要幾秒鍾的時間。 即使使用下面的示例,僅使用單個命令和按鈕以及命令管理器,按鈕也需要大約500毫秒來啟用/禁用自身。

我能想到的唯一原因是在CanExecute觸發之前CommandParameter綁定沒有更新,然后我猜想您無能為力。

提前致謝 :!

例如

假設我們有這個基本的ViewModel

public class BasicViewModel : INotifyPropertyChanged
{
    private string name;
    public string Name
    {
        get { return name; }
        set {
            this.name = value;
            RaisePropertyChanged("Name");
            Command.RaiseCanExecuteChanged();
        }
    }

    private Project project;

    public Project Project
    {
        get { return project; }
        set {
            if (project != null) project.PropertyChanged -= ChildPropertyChanged;
            if (value != null) value.PropertyChanged += ChildPropertyChanged;

            project = value;
            RaisePropertyChanged("Project");
        }
    }

    private void ChildPropertyChanged(object sender, PropertyChangedEventArgs e) {
        Command.RaiseCanExecuteChanged();
    }

    public DelegateCommand<string> Command { get; set; }

    public BasicViewModel()
    {
        this.Project = new Example.Project();
        Command = new DelegateCommand<string>(this.Execute, this.CanExecute);
    }

    private bool CanExecute(string arg) {
        return !string.IsNullOrWhiteSpace(arg);
    }

    private void Execute(string obj) { }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void RaisePropertyChanged(string propertyName = null) {
        if (this.PropertyChanged != null)
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

和這個模型

public class Project : INotifyPropertyChanged
{
    private string text;

    public string Text
    {
        get { return text; }
        set
        {
            text = value;
            RaisePropertyChanged("Text");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void RaisePropertyChanged(string propertyName = null)
    {
        var handler = this.PropertyChanged;
        if (handler != null)
            handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

現在在我看來,我有這個文本框和按鈕。

<Button Content="Button" CommandParameter="{Binding Path=Project.Text}" Command="{Binding Path=Command}" />
<TextBox Text="{Binding Path=Project.Text, UpdateSourceTrigger=PropertyChanged}" />

它的工作原理是,每當我在文本框中鍵入任何內容時,就會調用CanExecute,但該參數始終設置為先前的值。 假設我在文本框中寫了“ H”,則在參數設置為NULL的情況下觸發了CanExecute。 接下來,我寫“ E”,現在文本框包含“ HE”,並且CanExecute再次觸發。 這次僅將參數設置為“ H”。

由於某些奇怪的原因,該參數始終設置為先前的值,並且當我檢查Project.Text時將其設置為“ HE”,但參數仍僅設置為“ H”。

如果我現在將命令參數更改為

CommandParameter="{Binding Path=Name}"

和Textbox.Text到

Text={Binding Path=Name, UpdateSourceTrigger=PropertyChanged}"

一切正常。 CanExecute參數始終包含最新值,而不是先前值。

您正在談論的外觀模式是WPF的標准實踐。 這樣做的主要問題是,引發事件時,其訂閱的事件處理程序將按照訂閱的順序執行。 您所在的代碼行:

        if (value != null) value.PropertyChanged += ChildPropertyChanged;

這訂閱了“ Project”類的“ PropertyChanged”事件。 您的UIElement也通過在XAML中的綁定而訂閱了相同的“ PropertyChanged”事件。 簡而言之,您的“ PropertyChanged”事件現在有2個訂閱者。

關於事件的事情是,它們按順序觸發,並且代碼中發生的事情是,當事件從“ Project.Text”觸發時,它將執行“ ChildPropertyChanged”事件,並觸發“ CanExecuteChanged”事件,最終運行您的“ CanExecute”功能(當您看到錯誤的參數時)。 然后,在那之后,您的UIElements將獲得由同一事件執行的EventHandler。 它們的值會更新。

這是導致問題的訂閱順序。 試試這個,告訴我是否可以解決您的問題:

public Project Project
{
    get { return project; }
    set {
        if (project != null) project.PropertyChanged -= ChildPropertyChanged;
        project = value;
        RaisePropertyChanged("Project");
        if (project != null) project.PropertyChanged += ChildPropertyChanged;
    }
}

這就是我本應執行的操作,並且它按預期工作。 唯一的區別是我使用的是RelayCommand而不是DelegateCommand-它們從根本上具有相同的實現,因此應該可以互換。

當用戶輸入文本然后單擊按鈕時,RelayCommand的execute方法將獲得期望的文本-簡單。

XAML:

<Grid>

    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>

    <TextBox Grid.Column="0"
             Grid.Row="0"
             Text="{Binding Path=Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />

    <Button Grid.Column="0"
            Grid.Row="1"
            Content="Test"
            VerticalAlignment="Bottom"
            HorizontalAlignment="Center"
            Command="{Binding Path=TextCommand, Mode=OneWay}" />

</Grid>

視圖模型:

public sealed class ExampleViewModel : BaseViewModel
{
    private string _text;

    public ExampleViewModel()
    {
       TextCommand = new RelayCommand(TextExecute, CanTextExecute);
    }

    public string Text
    {
        get
        {
            return _text;
        }
        set
        {
            _text = value;
            OnPropertyChanged("Text");
        }
    }

    public ICommand TextCommand { get; private set; }

    private void TextExecute()
    {
        // Do something with _text value...
    }

    private bool CanTextExecute()
    {
        return true;
    }
}

我在棱鏡Codeplex討論論壇上發現了swythan的這一出色附件,效果非常好。 當然,它不能回答為什么將命令參數設置為先前的值,但是可以很好地解決該問題。

從源代碼進行了稍微的修改,從而在調用OnLoaded事件時調用HookCommandParameterChanged,從而有可能在TabItem中的控件上使用它。

public static class CommandParameterBehavior
{
    public static readonly DependencyProperty IsCommandRequeriedOnChangeProperty =
        DependencyProperty.RegisterAttached("IsCommandRequeriedOnChange",
                                            typeof(bool),
                                            typeof(CommandParameterBehavior),
                                            new UIPropertyMetadata(false, new PropertyChangedCallback(OnIsCommandRequeriedOnChangeChanged)));

    public static bool GetIsCommandRequeriedOnChange(DependencyObject target)
    {
        return (bool)target.GetValue(IsCommandRequeriedOnChangeProperty);
    }

    public static void SetIsCommandRequeriedOnChange(DependencyObject target, bool value)
    {
        target.SetValue(IsCommandRequeriedOnChangeProperty, value);
    }

    private static void OnIsCommandRequeriedOnChangeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (!(d is ICommandSource))
            return;

        if (!(d is FrameworkElement || d is FrameworkContentElement))
            return;

        if ((bool)e.NewValue)
            HookCommandParameterChanged(d);
        else
            UnhookCommandParameterChanged(d);

        UpdateCommandState(d);
    }

    private static PropertyDescriptor GetCommandParameterPropertyDescriptor(object source)
    {
        return TypeDescriptor.GetProperties(source.GetType())["CommandParameter"];
    }

    private static void HookCommandParameterChanged(object source)
    {
        var propertyDescriptor = GetCommandParameterPropertyDescriptor(source);
        propertyDescriptor.AddValueChanged(source, OnCommandParameterChanged);

        // N.B. Using PropertyDescriptor.AddValueChanged will cause "source" to never be garbage collected,
        // so we need to hook the Unloaded event and call RemoveValueChanged there.
        HookUnloaded(source);
    }

    private static void UnhookCommandParameterChanged(object source)
    {
        var propertyDescriptor = GetCommandParameterPropertyDescriptor(source);
        propertyDescriptor.RemoveValueChanged(source, OnCommandParameterChanged);

        UnhookUnloaded(source);
    }

    private static void HookUnloaded(object source)
    {
        var fe = source as FrameworkElement;
        if (fe != null)
        {
            fe.Unloaded += OnUnloaded;
            fe.Loaded -= OnLoaded;
        }

        var fce = source as FrameworkContentElement;
        if (fce != null)
        {
            fce.Unloaded += OnUnloaded;
            fce.Loaded -= OnLoaded;
        }
    }

    private static void UnhookUnloaded(object source)
    {
        var fe = source as FrameworkElement;
        if (fe != null)
        {
            fe.Unloaded -= OnUnloaded;
            fe.Loaded += OnLoaded;
        }

        var fce = source as FrameworkContentElement;
        if (fce != null)
        {
            fce.Unloaded -= OnUnloaded;
            fce.Loaded += OnLoaded;
        }
    }

    static void OnLoaded(object sender, RoutedEventArgs e)
    {
        HookCommandParameterChanged(sender);
    }

    static void OnUnloaded(object sender, RoutedEventArgs e)
    {
        UnhookCommandParameterChanged(sender);
    }

    static void OnCommandParameterChanged(object sender, EventArgs ea)
    {
        UpdateCommandState(sender);
    }

    private static void UpdateCommandState(object target)
    {
        var commandSource = target as ICommandSource;

        if (commandSource == null)
            return;

        var rc = commandSource.Command as RoutedCommand;
        if (rc != null)
            CommandManager.InvalidateRequerySuggested();

        var dc = commandSource.Command as IDelegateCommand;
        if (dc != null)
            dc.RaiseCanExecuteChanged();
    }
}

資料來源: https : //compositewpf.codeplex.com/discussions/47338

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM