WPFでボタンを有効/無効にする方法は?

私は比較的単純なWPFアプリケーションを持っています。アイデアは、ユーザにアイテムのリストを提示することである。各項目には項目を選択/選択解除するためのチェックボックスがあります。

私のコードは、単純化して、このように少し見える:

 class Thing { /* ... */ };
 public static int SelectedCount { get; set; }
 public class SelectableThing
 {
        private bool _selected;
        public bool Selected { 
            get { return _selected; }
            set { _selected = value; if (value) { SelectedCount++; } else { SelectedCount--; } } 
        }
        public Thing thing { get; set; }
 };
 private ObservableCollection _selectableThings;
 public Collection { get { return _selectableThings; } }
 
      
      
 
 <button Content="{Binding Path=SelectedTestCount}" Click="someFunc" />

つまり、ボタンの内容には選択したテストの数が表示されるはずです。これは、SelectableThing.Selectedが設定されるたびに、SelectedCountを適宜インクリメント/デクリメントする必要があるため、実行する必要があります。

しかし、限り、私は動作が動作しないことを伝えることができます。リスト内の項目の選択/選択解除にかかわらず、ボタンテキストには「0」が表示されます。

何か案は?

3
私はビューモデルを使用しています。
追加された 著者 Stephen Gross,
ビューモデルを使用しているのか、コードビハインドだけを使用していますか?
追加された 著者 Yatrix,

2 答え

この問題は、複数のビューモデルクラスが含まれているため、少し毛深いものです。これを解決するコードには亀裂があります。私が紛失している唯一の事は、行を離れるまで、DataGridがアイテムを更新しないように見えることです。

At start, before any edits. The save button is disabled, and the count is zero After some edits. The save button is enabled, and the count is updated

XAML:


    
        
            
            
        
        
            
                
                
            
        
        <button Content="{Binding Path=SelectedTestCount}"
                Command="{Binding SaveCommand}"
                Grid.Row="1" Width="75" Height="23"
                HorizontalAlignment="Right" Margin="0,0,6,6"/>
    

シングクラス:

public class Thing : INotifyPropertyChanged
{
    private readonly List selectableThings;
    private DelegateCommand saveCommand;

    public Thing(IEnumerable selectableThings)
    {
        this.selectableThings = new List(selectableThings);
        this.SelectableThings =
            new ObservableCollection(this.selectableThings);

        this.SaveCommand = this.saveCommand = new DelegateCommand(
            o => Save(),
            o => SelectableThings.Any(t => t.IsSelected)
            );

       //Bind children to change event
        foreach (var selectableThing in this.selectableThings)
        {
            selectableThing.PropertyChanged += SelectableThingChanged;
        }

        SelectableThings.CollectionChanged += SelectableThingsChanged;
    }

    public ObservableCollection SelectableThings
    {
        get;
        private set;
    }

    public int SelectedTestCount
    {
        get { return SelectableThings.Where(t => t.IsSelected).Count(); }
    }

    public ICommand SaveCommand { get; private set; }

    private void Save()
    {
       //Todo: Implement
    }

    private void SelectableThingChanged(object sender,
        PropertyChangedEventArgs args)
    {
        if (args.PropertyName == "IsSelected")
        {
            RaisePropertyChanged("SelectedTestCount");
            saveCommand.RaiseCanExecuteChanged();
        }
    }

    private void SelectableThingsChanged(object sender,
        NotifyCollectionChangedEventArgs e)
    {
        foreach (SelectableThing selectableThing in
            e.OldItems ?? new List())
        {
            selectableThing.PropertyChanged -= SelectableThingChanged;
            RaisePropertyChanged("SelectedTestCount");
        }

        foreach (SelectableThing selectableThing in
            e.NewItems ?? new List())
        {
            selectableThing.PropertyChanged += SelectableThingChanged;
            RaisePropertyChanged("SelectedTestCount");
        }
    }

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

    public event PropertyChangedEventHandler PropertyChanged;
}

Selectableシングクラス:

public class SelectableThing : INotifyPropertyChanged
{
    private string name;
    private bool isSelected;

    public SelectableThing(string name)
    {
        this.name = name;
    }

    public string Name
    {
        get { return name; }
        set
        {
            name = value;
            RaisePropertyChanged("Name");
        }
    }

    public bool IsSelected
    {
        get { return isSelected; }
        set
        {
            isSelected = value;
            RaisePropertyChanged("IsSelected");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

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

元の回答:

CommandICommand にバインドします。あなたの条件が満たされていないときに false を返すように ICommandCanExecute を設定します。

その IsSelected プロパティの設定で、値が変更されたら CanExecuteChanged イベントを発生させます。

ButtonCommand バインディングは、 CanExecute の結果に基づいて自動的にボタンを有効/無効にします。

ここで使用できる ICommand の実装など、これを行う方法の詳細については、このミニMVVMチュートリアル別の質問のために書きました。

CanExecute の実装を記入するには、Linqの .Any メソッドのようなものを使用します。次に、 Count のチェックを気にする必要はなく、チェックされているアイテムがあれば早期にループを終了させることができます。

例えば:

this.SaveCommand = new DelegateCommand(Save, CanSave);

// ...

private void Save(object unusedArg)
{
   //Todo: Implement
}

private bool CanSave(object unusedArg)
{
    return SelectableThings.Any(t => t.IsSelected);
}

または短いので、ラムダインラインを使用します。

this.SaveCommand = new DelegateCommand(Save,
    o => SelectableThings.Any(t => t.IsSelected)
    );
3
追加された
子のビューモデルのコレクションを使用しているように見えるので、 IsSelected の変更に基づいて CanExecuteChanged を上げることについての注意は難しいかもしれません。それぞれに INotifyPropertyChanged を登録して、このイベントを this.SaveCommand.RaiseCanExecuteChanged()に転送することができます。もしあなたがこの部分を理解するのに困っていることがあれば教えてください。
追加された 著者 Merlyn Morgan-Graham,
また、 IsSelected という名前の名前を付けることをお勧めします。ブール値(Is、Can、Hasなど)に名前を付けるときは、この規則に従ってください。
追加された 著者 Merlyn Morgan-Graham,
@StephenGross:静的メンバーを使用することができますが、それはハックです。そのプロパティを保持しているクラス( Thing ?)の複数のインスタンスをサポートすることはできませんでした。また、このコードの単体テストを書くことも難しくなります。また、WPFが静的プロパティにバインドできるかどうか、または INotifyPropertyChanged が機能するかどうかはわかりません。
追加された 著者 Merlyn Morgan-Graham,
@Stephen:この問題は一種の厄介なものなので、私が意味することを正確に示すためにいくつかのコードを追加しました。私のソリューションは、あなたのために INotifyPropertyChanged のいくつかをラップするツールキットを使用せずに、きれいなものに近づけるほど近くにあります。
追加された 著者 Merlyn Morgan-Graham,
@Stephen:問題は、1つのチェックボックスが変更されるたびに、ボタン/テキストボックスを更新する必要があることです。これを行う唯一の方法は、すべてのイベントを購読することです。 WPFはイベントごとのチェックボックスにサブスクライブしますが、集約されたすべてのイベントを簡単に購読する方法はありません。チェックボックスでトリガを使用して解決することができますが、これを実現するにはバックエンドコンバータのコードが必要になるかもしれませんが(この回答で取り残されているLinqに似た実装があります)。
追加された 著者 Merlyn Morgan-Graham,
@Stephen:私が提案する方法とは異なる方法で動作させる場合は、別の答えとして追加して受け入れることをお勧めします。
追加された 著者 Merlyn Morgan-Graham,
あなたの意見を反映するために元の質問を修正しました。あなたは私にあなたの考えを知らせることができますか?
追加された 著者 Stephen Gross,
ええ、私が静的intを使用した唯一の理由は、そうでなければSelectableThing.Selected.set()内から変更できないことです。
追加された 著者 Stephen Gross,
さて、私はあなたの修正された勧告を見ています。それはより簡単な方法があるように思えます...ブールIsSelected変更時に設定される 'int SelectedCount'属性を持つことは可能でしょうか?そうであれば、コントロールをSelectedCountにバインドして、その方法でドライブできるはずです。
追加された 著者 Stephen Gross,

ボタンの内容をviewmodelのフィールドにバインドし、別の項目が選択または選択解除されるたびにそのフィールドのOnChangedメソッドを起動します。 IsEnabledをビューモデルのブール値フィールドにバインドし、必要に応じてtrue/falseに設定して、ボタンを有効または無効にします。

1
追加された
私はこれを試して元の質問を修正したが、おそらく間違っている(!)...
追加された 著者 Stephen Gross,