Hoe een knop in WPF aan/uit te zetten?

Ik heb een relatief eenvoudige WPF-applicatie. Het idee is om een ​​lijst met items aan de gebruiker te presenteren; voor elk item is er een selectievakje om het item te selecteren/deselecteren.

Mijn code, vereenvoudigd, ziet er ongeveer zo uit:

 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" />

Dus het idee is dat de knopinhoud het aantal geselecteerde tests moet weergeven. Dit moet worden uitgevoerd omdat, wanneer SelectableThing.Selected is ingesteld, de SelectedCount indien nodig moet worden verhoogd/verlaagd.

Voor zover ik kan vertellen, werkt het gedrag echter niet. De knoptekst geeft "0" weer, ongeacht het selecteren of deselecteren van items in de lijst.

Om het even welke ideeën?

3
Ik gebruik viewmodels.
toegevoegd de auteur Stephen Gross, de bron
Gebruik je viewmodellen of gewoon code achter?
toegevoegd de auteur Yatrix, de bron

2 antwoord

Dit probleem is een beetje behaard omdat je meerdere klassen van viewmodellen hebt gebruikt. Hier is een scheur in de code om dit op te lossen. Het enige wat ik mis is dat de DataGrid je items niet lijkt bij te werken tot je de rij verlaat.

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"/>
    

Dingles:

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;
}

SelectableDingles:

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));
        }
    }
}

Oorspronkelijk antwoord:

Bind Command aan een ICommand . Stel uw CanExecute in op de ICommand om false te retourneren wanneer niet aan uw voorwaarde is voldaan.

In de setter voor die eigenschap IsSelected verhoogt u de CanExecuteChanged -gebeurtenis wanneer de waarde verandert.

De Command binding op een knop schakelt de knop automatisch in/uit op basis van het resultaat van CanExecute .

Zie deze mini-MVVM-handleiding Ik schreef me op voor een andere vraag.

Om de implementatie van CanExecute in te vullen, zou ik iets als Linq's .Any -methode gebruiken. Dan hoeft u niet de moeite te nemen om Count te controleren en kunt u de lus vroegtijdig beëindigen als u vindt dat een item is aangevinkt.

Bijvoorbeeld:

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

// ...

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

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

Of, omdat het kort is, gebruik een lambda inline:

this.SaveCommand = new DelegateCommand(Save,
    o => SelectableThings.Any(t => t.IsSelected)
    );
3
toegevoegd
Het bit over het verhogen van CanExecuteChanged op basis van IsSelected wijzigen kan lastig zijn, omdat het lijkt alsof u een verzameling modellen voor onderliggende weergave gebruikt. U kunt zich op elke code abonneren op INotifyPropertyChanged en de gebeurtenis doorsturen naar this.SaveCommand.RaiseCanExecuteChanged() . Laat me weten of je problemen hebt om dit deel uit te zoeken.
toegevoegd de auteur Merlyn Morgan-Graham, de bron
Merk ook op Ik stel voor dat u de eigenschap IsSelected benoemt. U moet deze conventie volgen bij het benoemen van Booleaanse waarden (Is, Can, Has, enz.).
toegevoegd de auteur Merlyn Morgan-Graham, de bron
@StephenGross: tijdens het gebruik van een statisch -lid kan het werken, het is een hack. U kunt niet meerdere instanties ondersteunen van elke klasse die deze eigenschap bevat ( Thing ?). Het zal ook moeilijker zijn om eenheidstests voor deze code te schrijven. Ik weet ook niet zeker of wpf kan binden aan statische eigenschappen of niet, of dat INotifyPropertyChanged erop zou werken.
toegevoegd de auteur Merlyn Morgan-Graham, de bron
@Stephen: Ik heb wat code toegevoegd om precies te laten zien wat ik bedoel, aangezien dit probleem nogal smerig is. Mijn oplossing is ongeveer net zo dichtbij als je een schone kunt bereiken zonder een toolkit te gebruiken die een deel van het INotifyPropertyChanged spul voor je verpakt.
toegevoegd de auteur Merlyn Morgan-Graham, de bron
@Stephen: het probleem is dat elke keer dat een selectievakje wordt gewijzigd, u de knop/het tekstvak moet bijwerken. De enige manier om dit te doen is om je te abonneren op alle evenementen. wpf abonneert zich op het per-checkbox-evenement, maar heeft geen eenvoudige manier om zich te abonneren op alle geaggregeerde evenementen. Je zou kunnen kijken naar een trigger op de selectievakjes om het op te lossen, hoewel je misschien een back-end convertercode nodig hebt om dat te laten werken (met implementaties vergelijkbaar met de Linq die ik in dit antwoord heb geplakt).
toegevoegd de auteur Merlyn Morgan-Graham, de bron
@Stephen: als je het aan de praat krijgt met een andere methode dan ik, dan raad ik je aan om het als een apart antwoord toe te voegen en het te accepteren.
toegevoegd de auteur Merlyn Morgan-Graham, de bron
Ik heb de oorspronkelijke vraag aangepast om uw inbreng weer te geven; kan je me laten weten wat je ervan vindt?
toegevoegd de auteur Stephen Gross, de bron
Ja, de enige reden dat ik een statische int heb gebruikt, is dat je anders niet van binnenuit SelectableThing.Selected.set() kunt veranderen.
toegevoegd de auteur Stephen Gross, de bron
Oké, ik bekijk uw aangepaste aanbeveling. Het lijkt erop dat er een eenvoudiger manier zou zijn ... Is het niet mogelijk om een ​​'int SelectedCount' attribuut te hebben dat wordt ingesteld wanneer de boolean IsSelected verandert? Als dat zo is, zou ik in staat moeten zijn om de controls aan de SelectedCount te verbinden en ze op die manier te besturen.
toegevoegd de auteur Stephen Gross, de bron

Bind de inhoud van de knop aan een veld in uw viewmodel en activeer de OnChanged-methode voor dat veld telkens wanneer een ander item wordt geselecteerd of uitgeschakeld. Koppel de IsEnabled aan een Boolean-veld in uw weergavemodel en stel deze waar/onwaar in om de knop in of uit te schakelen.

1
toegevoegd
Ik heb de oorspronkelijke vraag aangepast om dit uit te proberen, maar waarschijnlijk heb ik het verkeerd gedaan (!) ...
toegevoegd de auteur Stephen Gross, de bron