none
WPF Command CanExecute aktualisieren RRS feed

  • 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?

    Montag, 14. September 2015 09:13

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



    Montag, 14. September 2015 15:45
  • 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
    Dienstag, 15. September 2015 09:54

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

    Montag, 14. September 2015 10:57
  • 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

    Montag, 14. September 2015 11:45
    Moderator
  • 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.

    Montag, 14. September 2015 11:52
  • 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


    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!

    Montag, 14. September 2015 14:29
  • 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

    Montag, 14. September 2015 14:57
  • 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!

    Montag, 14. September 2015 15:18
  • 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

    Montag, 14. September 2015 15:29
    Moderator
  • 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



    Montag, 14. September 2015 15:45
  • 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

    Montag, 14. September 2015 15:50
  • 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
    Dienstag, 15. September 2015 09:54
  • 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

    Dienstag, 15. September 2015 11:51
  • 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");
            }

    Dienstag, 15. September 2015 12:11
  • 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

    Dienstag, 15. September 2015 12:45