Benutzer mit den meisten Antworten
WPF Command CanExecute aktualisieren

Frage
-
Moin zusammen,
ich habe einen Command an einen Button gebunden und das funktioniert auch soweit. Nun würde ich gerne, dass der Button seinen Status ändert, wenn sich im ViewModel eine bestimmte Eigenschaft ändert. Das klappt leider nicht SOFORT sondern erst wenn ich eine Aktion in der View vornehme. Also die View den Fokus erhält oder ich irgendwo hin klicke.
<Button Content="Reset" Command="{Binding IOZurücksetzenCommand,UpdateSourceTrigger=PropertyChanged}" Width="50" HorizontalAlignment="Left"/>
Was muss man genau einstellen werden, damit der Button sofort seinen Status ändert und nicht erst nach einer Aktion in der UI?
Antworten
-
Hi David,
Du weist SubText="55" in einem anderen thread zu. Diese Zuweisung führt zur Ausführung des Setters in SubText auch im Context des anderen threads. Dort rufst Du OnPropertyChanged ohne Parameter auf. Wegen dem fehlenden Parameter wird CallerMemberName genutzt, d.h. "SubText". Die Ereignisroutine ist letztendlich ein Delegate, der aufgerufen wird, immer noch im Context des anderen threads. Die UI läuft aber im Haupt-Thread. Solche thread-übergreifende Zugriffe sollten vermieden werden, z.B. mit dem Dispatcher, BackgroundWorker, SynchronizationContext.Post o.ä.Auch, wenn das "zufällig" funktioniert, hast Du damit noch nicht die Eigenschaft "IOZurücksetzenCommand" in Deinem XAML aktualisiert. Entweder Du machst das direkt oder über String.Empty für die gesamte Oberfläche.
--
Viele Grüsse
Peter Fleischer (MVP, Partner)
Meine Homepage mit Tipps und Tricks
- Bearbeitet Peter Fleischer Montag, 14. September 2015 15:46
- Als Antwort markiert David Stania Dienstag, 15. September 2015 10:47
-
Hallo zusammen,
ich habe es nun mit dem Dispatcher gelöst. Herzlichen Dank für die Unterstützung! Als Zusammenfassung hier nochmal der Code des Beispiels mit der Lösung:
ViewModel:
class Window04VM : INotifyPropertyChanged { private string _text1; public string Text1 { get { return this._text1; } set { if (this._text1 != value) { this._text1 = value; this.Text2 = value; OnPropertyChanged(); OnPropertyChanged("Cmd"); } } } public string Text2 { get; set; } public SubKlasse meineSubKlasse { get; set; } public ICommand Cmd { get { return new RelayCommand(CmdExec, canExec); } } private bool canExec() { //return Text1 == "55"; return meineSubKlasse.Text1 == "55"; } private void CmdExec() { // TuWas; } public Window04VM() { meineSubKlasse = new SubKlasse(); System.Threading.Tasks.Task.Factory.StartNew(() => { System.Threading.Thread.Sleep(3000); Text1 = "55"; }); } #region OnPropertyChanged public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged([CallerMemberName] string propname = "") { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propname)); } #endregion }
SubKlasse mit dem Dispatcher:
public class SubKlasse : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged([CallerMemberName] string propname = "") { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propname)); } private string text1; public string Text1 { get { return text1; } set { text1 = value; OnPropertyChanged(); } } public SubKlasse() { System.Threading.Tasks.Task.Factory.StartNew(() => { System.Threading.Thread.Sleep(3000); Application.Current.Dispatcher.Invoke(new Action(() => { Text1 = "55"; })); }); } }
View:
<Window.Resources> <local:Window04VM x:Key="vm"/> </Window.Resources> <StackPanel DataContext="{Binding Source={StaticResource vm}}"> <Label Content="TextBox 1" /> <TextBox Text="{Binding meineSubKlasse.Text1, UpdateSourceTrigger=PropertyChanged}"/> <Label Content="TextBox 2" /> <TextBox Text="{Binding Text2}"/> <Button Command="{Binding Cmd}" Content="nur bei TextBox1 = 55" /> </StackPanel>
- Als Antwort markiert David Stania Dienstag, 15. September 2015 10:47
Alle Antworten
-
Hi David,
die einfachste Möglichkeit ist ein PropertyChanged ohne Angabe einer Eigenschaft (nur String.Empty angeben). Damit wird die gesamte Oberfläche aktualisiert.--
Viele Grüsse
Peter Fleischer (MVP, Partner)
Meine Homepage mit Tipps und Tricks -
Hallo David,
wenn sich eine Eigenschaft ändert muss man der View manuell mitteilen das sich die Eigenschaft änderte und sich die UI entsprechend zu aktualisieren hat. Dies macht man i.d.R. über das PropertyChanged Event welches durch die INotifyPropertyChanged-Schnittstelle implementiert wird.
Siehe auch: Gewusst wie: Implementieren der INotifyPropertyChanged-Schnittstelle
In den EventArgs gibt man dabei den Namen der Eigenschaft an die zu aktualisieren ist.
Tom Lambert - .NET (C#) MVP
Wozu Antworten markieren und für Beiträge abstimmen? Klicke hier.
Nützliche Links: .NET Quellcode | C# ↔ VB.NET Konverter | Account bestätigen (Verify Your Account)
Ich: Webseite | Code Beispiele | Facebook | Twitter | Snippets -
Hallo zusammen,
danke für die Antworten! Die Schnittstelle ist selbstverständlich im Model implementiert! Nur habe ich in meinem ViewModel die Commands. Wie stoße ich also durch das INotifyPropertyChanged in dem Model, die Funktion CanExecute in meinem ViewModel an? :-) Ich bin davon ausgegangen, dass durch das INotifyPropertyChanged eben auch alle Commands geprüft werden.
-
Hi David,
Du musst über NotifyPropertyChanged sicherstellen, dass die Oberfläche die Eigenschaften erneut abruft, die für die Darstellung relevant sind. Für Deinen Fall musst Du im Fall, wenn der Button anders darzustellen ist, aufrufen:... PropertyChanged(this , new PropertyChangedEventArgs("IOZurücksetzenCommand"));
Hier mal eine Demo:
XAML:
<Window x:Class="WpfApplication1CS.Window04" 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:WpfApplication1CS" mc:Ignorable="d" Title="Window04" Height="300" Width="300"> <Window.Resources> <local:Window04VM x:Key="vm"/> </Window.Resources> <StackPanel DataContext="{Binding Source={StaticResource vm}}"> <Label Content="TextBox 1" /> <TextBox Text="{Binding Text1, UpdateSourceTrigger=PropertyChanged}"/> <Label Content="TextBox 2" /> <TextBox Text="{Binding Text2}"/> <Button Command="{Binding Cmd}" Content="nur bei TextBox1 = 55" /> </StackPanel> </Window>
Der ViewModel dazu:
using System.ComponentModel; using System.Runtime.CompilerServices; using System.Windows.Input; namespace WpfApplication1CS { class Window04VM : INotifyPropertyChanged { private string _text1; public string Text1 { get { return this._text1; } set { if (this._text1 != value) { this._text1 = value; this.Text2= value; OnPropertyChanged(); OnPropertyChanged("Cmd"); } } } public string Text2 { get; set; } public ICommand Cmd { get { return new RelayCommand(CmdExec, canExec); } } private void CmdExec(object obj) { // TuWas; } private bool canExec(object obj) { return Text1 == "55"; } #region OnPropertyChanged public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged([CallerMemberName] string propname = "") { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propname)); } #endregion } }
--
Viele Grüsse
Peter Fleischer (MVP, Partner)
Meine Homepage mit Tipps und Tricks
- Bearbeitet Peter Fleischer Montag, 14. September 2015 13:36
-
Hallo Peter,
so funktioniert es natürlich 1A! Sobald man eine Taste betätigt, werden ja auch die Commands neu geprüft!
Bei mir ist der Aufbau wie folgt:
XAML:
<Window.Resources> <local:Window04VM x:Key="vm"/> </Window.Resources> <StackPanel DataContext="{Binding Source={StaticResource vm}}"> <Label Content="TextBox 1" /> <TextBox Text="{Binding meineSubKlasse.SubText, UpdateSourceTrigger=PropertyChanged}"/> <Label Content="TextBox 2" /> <TextBox Text="{Binding Text2}"/> <Button Command="{Binding Cmd}" Content="nur bei TextBox1 = 55" /> </StackPanel>
ViewModel:
class Window04VM : INotifyPropertyChanged { private string _text1; public string Text1 { get { return this._text1; } set { if (this._text1 != value) { this._text1 = value; this.Text2 = value; OnPropertyChanged(); OnPropertyChanged("Cmd"); } } } public string Text2 { get; set; } public SubKlasse meineSubKlasse { get; set; } public ICommand Cmd { get { return new RelayCommand(CmdExec, canExec); } } private bool canExec() { // return Text1 == "55"; return meineSubKlasse.SubText == "55"; } private void CmdExec() { // TuWas; } public Window04VM() { meineSubKlasse = new SubKlasse(); } #region OnPropertyChanged public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged([CallerMemberName] string propname = "") { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propname)); } #endregion }
SubKlasse:
public class SubKlasse : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged([CallerMemberName] string propname = "") { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propname)); } private string subText; public string SubText { get { return subText; } set { subText = value; OnPropertyChanged(); } } public SubKlasse() { System.Threading.Tasks.Task.Factory.StartNew(() => { System.Threading.Thread.Sleep(3000); SubText = "55"; }); } }
Sobald ich manuell die Zahl 55 einfüge, funktioniert es natürlich super mit dem Command. Sobald man aber auf den anderen Thread wartet... passiert nichts!
-
Hi David,
Dein OnPropertyChanged in der Subklasse wird in einem anderen (nicht dem UI-Thread) aufgerufen. Ob das funktionieren kann, bezweifle ich. Führe des OnPropertyChanged mal im UI-Thread aus (über Dispatcher, SynchronizationContext o.ä.).--
Viele Grüsse
Peter Fleischer (MVP, Partner)
Meine Homepage mit Tipps und Tricks -
Hallo Peter,
und genau das ist das was ich nicht verstehe!
Es ist ja quasi im selben Thread! Es wird nur aus einem anderen zugewiesen.
Wenn ich die Subklasse rausnehme und in deinem ersten Demo-Beispiel im Konstruktor den folgenden Code eintrage:
public Window04VM() { // meineSubKlasse = new SubKlasse(); System.Threading.Tasks.Task.Factory.StartNew(() => { System.Threading.Thread.Sleep(5000); Text1 = "55"; }); }
Funktioniert auch alles super mit den Commands!
-
Hallo David,
ich glaube das hier 2 Teilprobleme vorliegen. Das erste ist das Threading. Vereinfacht gesagt kann es passieren das der UI Thread einfach nicht mit bekommt das sich eine Variable geändert hat, sofern dies in einem anderen Thread geschah. Dafür musst du den Dispatcher ins Spiel bringen:
Application.Current.Dispatcher.BeginInvoke(new Action(() => { SubText = "55"; }));
Weiterhin würde ich erwarten dass man der UI auch noch mitteilen muss das sich CanExecute geändert hat. Das könnte dann beispielsweise so aussehen:public Window04VM() { _Cmd = new RelayCommand(CmdExec, canExec); meineSubKlasse = new SubKlasse(); } public SubKlasse _meineSubKlasse; public SubKlasse meineSubKlasse { get { return _meineSubKlasse; } set { if (_meineSubKlasse != null) _meineSubKlasse.PropertyChanged -= OnSubPropertyChanged; _meineSubKlasse = value; _meineSubKlasse.PropertyChanged += OnSubPropertyChanged; } } private void OnSubPropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == "SubText") { Cmd.RaiseCanExecuteChanged(); } } readonly RelayCommand _Cmd; public RelayCommand Cmd { get { return _Cmd; } }
Beachte bitte das ICommand die RaiseCanExecuteChanged Methode nicht mit bringt, sondern das diese vom verwendeten Framework und der verwendeten Klasse abhängt.
Tom Lambert - .NET (C#) MVP
Wozu Antworten markieren und für Beiträge abstimmen? Klicke hier.
Nützliche Links: .NET Quellcode | C# ↔ VB.NET Konverter | Account bestätigen (Verify Your Account)
Ich: Webseite | Code Beispiele | Facebook | Twitter | Snippets -
Hi David,
Du weist SubText="55" in einem anderen thread zu. Diese Zuweisung führt zur Ausführung des Setters in SubText auch im Context des anderen threads. Dort rufst Du OnPropertyChanged ohne Parameter auf. Wegen dem fehlenden Parameter wird CallerMemberName genutzt, d.h. "SubText". Die Ereignisroutine ist letztendlich ein Delegate, der aufgerufen wird, immer noch im Context des anderen threads. Die UI läuft aber im Haupt-Thread. Solche thread-übergreifende Zugriffe sollten vermieden werden, z.B. mit dem Dispatcher, BackgroundWorker, SynchronizationContext.Post o.ä.Auch, wenn das "zufällig" funktioniert, hast Du damit noch nicht die Eigenschaft "IOZurücksetzenCommand" in Deinem XAML aktualisiert. Entweder Du machst das direkt oder über String.Empty für die gesamte Oberfläche.
--
Viele Grüsse
Peter Fleischer (MVP, Partner)
Meine Homepage mit Tipps und Tricks
- Bearbeitet Peter Fleischer Montag, 14. September 2015 15:46
- Als Antwort markiert David Stania Dienstag, 15. September 2015 10:47
-
Hi Tom,
ich nutze da immer eine RelayCommand-Klasse, die im Konstruktor gleich eine booleasche Funktion mitgibt, die den Button freigibt oder sperrt. Das erfordert aber bei bestimmen Vorlagen, die in der Vorlage enthaltene RelayCommand-Klasse auszutauschen. Warum das Microsoft so gemacht hat, kann ich nicht sagen. Ich empfinde es als nicht optimal.--
Viele Grüsse
Peter Fleischer (MVP, Partner)
Meine Homepage mit Tipps und Tricks -
Hallo zusammen,
ich habe es nun mit dem Dispatcher gelöst. Herzlichen Dank für die Unterstützung! Als Zusammenfassung hier nochmal der Code des Beispiels mit der Lösung:
ViewModel:
class Window04VM : INotifyPropertyChanged { private string _text1; public string Text1 { get { return this._text1; } set { if (this._text1 != value) { this._text1 = value; this.Text2 = value; OnPropertyChanged(); OnPropertyChanged("Cmd"); } } } public string Text2 { get; set; } public SubKlasse meineSubKlasse { get; set; } public ICommand Cmd { get { return new RelayCommand(CmdExec, canExec); } } private bool canExec() { //return Text1 == "55"; return meineSubKlasse.Text1 == "55"; } private void CmdExec() { // TuWas; } public Window04VM() { meineSubKlasse = new SubKlasse(); System.Threading.Tasks.Task.Factory.StartNew(() => { System.Threading.Thread.Sleep(3000); Text1 = "55"; }); } #region OnPropertyChanged public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged([CallerMemberName] string propname = "") { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propname)); } #endregion }
SubKlasse mit dem Dispatcher:
public class SubKlasse : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged([CallerMemberName] string propname = "") { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propname)); } private string text1; public string Text1 { get { return text1; } set { text1 = value; OnPropertyChanged(); } } public SubKlasse() { System.Threading.Tasks.Task.Factory.StartNew(() => { System.Threading.Thread.Sleep(3000); Application.Current.Dispatcher.Invoke(new Action(() => { Text1 = "55"; })); }); } }
View:
<Window.Resources> <local:Window04VM x:Key="vm"/> </Window.Resources> <StackPanel DataContext="{Binding Source={StaticResource vm}}"> <Label Content="TextBox 1" /> <TextBox Text="{Binding meineSubKlasse.Text1, UpdateSourceTrigger=PropertyChanged}"/> <Label Content="TextBox 2" /> <TextBox Text="{Binding Text2}"/> <Button Command="{Binding Cmd}" Content="nur bei TextBox1 = 55" /> </StackPanel>
- Als Antwort markiert David Stania Dienstag, 15. September 2015 10:47
-
Hi David,
Deine Lösung funktioniert nicht.Der Dispatcher schreibt in die Eigenschaft "Text1" Deines Objektes meineSubKlasse (vom Typ Subklasse). Diese Änderung bewirkt keine Benachrichtigung der Eigenschaft "Cmd" und damit wird die Anzeige des Buttons auch nicht verändert.
--
Viele Grüsse
Peter Fleischer (MVP, Partner)
Meine Homepage mit Tipps und Tricks -
Oh man das macht mich langsam fertig -_-
Wenn ich das OnPropertyChanged-Event aus der SubKlasse abonniere, dann kann ich auf eine Änderung in der SubKlasse in der Hauptklasse reagieren und einen Aufruf von "Cmd" herbeiführen richtig? Also würde das hier schon reichen?
public Window04VM() { meineSubKlasse = new SubKlasse(); meineSubKlasse.PropertyChanged += meineSubKlasse_PropertyChanged; } void meineSubKlasse_PropertyChanged(object sender, PropertyChangedEventArgs e) { OnPropertyChanged("Cmd"); }
-
Hi David,
ich kenne Deine konkrete Aufgabenstellung nicht und kann deshalb auch nicht erkennen, warum Du über mehrere Schichten so etwas veranstalten willst/musst. Für solch einen Fall ist es besser, in der Oberfläche nur Eigenschaften des ViewModels zu binden. Wenn der ViewModel Subklassen-Objekte nutzt, dann sollten diese Subklassen-Objekte Datenänderungen im ViewModel bewirken, die dann wiederum die Oberfläche informieren.--
Viele Grüsse
Peter Fleischer (MVP, Partner)
Meine Homepage mit Tipps und Tricks