Command erstellen C#

toma123

Banned
Registriert
Apr. 2020
Beiträge
100
Habe mich nochmals durch verschiedene Tutorial gelesen und habe jetzt eine einfach Möglichkeit um meine Buttons einen Command zuzuweisen erstellt.

Wärt ihr so nett und schaut mal darüber ob es jetzt so passt?

1. Klasse: MyCommands erstellt
2. Von Icommand erben lassen und Schnittstelle implementiert
C#:
public class CopyCommand : ICommand
    {
        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter)
        {
            return true;
        }

        public void Execute(object parameter)
        {
            Messagebox.Show("funzt");
            
        }
    }

3.in Code Behind Datei, Command initialisieren und Datacontext eingefügt
C#:
   public ICommand ShowCommand { get; set; } = new ShowCommand();
   
   DataContext = this;



4. in XAML Button und Text gebunden-

XML:
   <Button Command="{Binding Path=ShowCommand}" CommandParameter="{Binding Text, ElementName=tb_ShowID}">
                    <TextBlock Name="tb_ShowID" />
 
Dein Ansatz ist schon mal nicht schlecht, ich würde aber das ganze noch etwas weiter treiben und eine eigene, generische Command-Klasse anlegen, damit du nicht für jedes einzelne Command immer wieder ne eigene Klasse benötigst:
C#:
public class MyCommand : ICommand
{
    private readonly Action _targetExecuteMethod;
    private readonly Func<bool> _targetCanExecuteMethod;

    public MyCommand( Action targetExecuteMethod, Func<bool> targetCanExecuteMethod )
    {
        _targetExecuteMethod = targetExecuteMethod;
        _targetCanExecuteMethod = targetCanExecuteMethod;
    }
   
    // Default Konstruktor, wenn man die Ausführbarkeit nicht definieren möchte
    public MyCommand(Action targetExecuteMethod) : this(targetExecuteMethod, () => true) { }

    public void RaiseCanExecuteChanged()
    {
        CanExecuteChanged?.Invoke( this, EventArgs.Empty );
    }

    public bool CanExecute( object parameter = null )
    {
        return _targetCanExecuteMethod?.Invoke( ) ?? false;
    }

    public void Execute( object parameter = null )
    {
        _targetExecuteMethod?.Invoke( );
    }

    // weak reference, if command instance lifetime is longer than lifetime of UI objects hooked up to the command
    public event EventHandler CanExecuteChanged = delegate { };
}

Eigentlich brauchst du hier auch keinen Parameter, sondern speicherst den Text per Binding in deinem ViewModel. Damit hast du, wie auch das Command, beides in deinem ViewModel-Kontext und kannst easy darauf zugreifen. Wichtig ist hierbei die Implementierung von INotifyPropertyChanged (und sofern man Text immer eingeben kann im XAML das Binding TwoWay einzurichten per Binding CopyText, UpdateSourceTrigger=TwoWay).

Ein neues Command würdest du dann im ViewModel so erzeugen:
C#:
public MyCommand CopyCommand { get; set; }
public string CopyText {
    get => copyText;
    set {
        if(copyText.Equals(value))
            return;
        copyText = value;
        NotifyPropertyChanged(nameof(CopyText));
        CopyCommand.RaiseCanExecuteChanged();
    }
}
...
// Initialisierung (im Konstruktor oder in eigener Init-Methode gekapselt für Coding Style)
private void Init(){
    CopyCommand = new MyCommand(Copy, CanCopy);
}
...
private void Copy(){
    // Hier kommt die Kopierlogik hin
}

private bool CanCopy(){
    return !string.IsNullOrEmpty(CopyText);
}
 
  • Gefällt mir
Reaktionen: Hayda Ministral
Da freue mich erst einmal, dass ich das ganze jetzt so begriffen habe. Werde jetzt Schritt für Schritt weitermachen, damit ich alles auch verstanden habe was ich so geschrieben hab.
 
  • Gefällt mir
Reaktionen: Nero1
Hab das ganze jetzt nochmals weiter gesponnen aber irgendwo hänge ich erneut. Auch wenn es noch einen anderen Weg gibt, wüsste ich gern wo mein Fehler liegt. Ist besser für mich zum Verständnis.

In meiner XML binde ich den Text der Statusbar
XML:
<StatusBar Grid.Column="0" Grid.Row="7" Grid.ColumnSpan="3">
  <StatusBarItem>
       <TextBlock Width="300" Text="{Binding StatusText}" />
  </StatusBarItem>
</StatusBar>

2. Außerdem habe ich eine MainWindowViewModel Klasse angelegt, die von INotifyPropertyChanged erbt und sie implementiert
2.1 Zusätzlich habe ich das Commando mit eingebunden und aus der XAML Code Behind gelöscht sowie die MainWindowViewModel Klasse als Dataindex angegeben.

So so sieht dieses nun aus:

C#:
{
    class MainWindowViewModel : INotifyPropertyChanged
    {
        string _statusText;
        public string StatusText
        {
            get { return _statusText; }
            set
            {
                _statusText = value;
             }
        }

    public ICommand ShowCommand { get; set; } = new ShowCommand();

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

        public event PropertyChangedEventHandler PropertyChanged;
    }


3. Die extra Klasse für Commands habe ich belassen aber noch im Execute das MainViewModel instanziert und den StatusTest gesetzt.

C#:
public class CopyCommand : ICommand
    {
        public event EventHandler CanExecuteChanged;
        public bool CanExecute(object parameter)
        {
            return true;
        }
        public void Execute(object parameter)
        {
            Messagebox.Show("funzt");
            MainWindowViewModel mvw = new MainWindowViewModel();
            mvw.StatusText = "gesetzt";
        }
    }

Das Kommando funktioniert weiterhin, allerdings wird mein Text in der Statusbar nicht gesetzt. Mache ich etwas falsch oder übersehe ich etwas?
 
toma123 schrieb:
Außerdem habe ich eine MainWindowViewModel Klasse angelegt, die von INotifyPropertyChanged erbt und sie implementiert
Von Interfaces kann nichts geerbt werden, diese werden lediglich implementiert (da sie keine eigene Logik enthalten). Vererbt werden nur abstrakte Klassen/Methoden :)
toma123 schrieb:
aber noch im Execute das MainViewModel instanziert und den StatusTest gesetzt.
toma123 schrieb:
allerdings wird mein Text in der Statusbar nicht gesetzt.
Hier spielen zwei Faktoren zusammen. Erstens hast du zwar INotifyPropertyChanged implementiert, verwendest es aber nicht. Damit der Statustext bei Aktualisierung auch die Änderung anzeigt musst du der UI Bescheid geben, dass sich etwas geändert hat.
C#:
string _statusText;
public string StatusText
{
    get { return _statusText; }
    set
    {
        // keine Aktualisierung wenn sich nichts verändert hat
        if(_statusText == value)
            return;
        _statusText = value;
        // Benachrichtigung an die UI
        OnPropertyChanged(nameof(StatusText));
    }
}

Dazu kommt, dass du das ViewModel als DataContext gesetzt hast. Das Setzen erzeugt eine Instanz des ViewModels, in dem auch die Bindings gebunden sind. Was du im Command machst ist eine völlig neue Instanz des ViewModels zu erzeugen, die nichts mit dem DataContext zu tun hat, der deinem Window zugewiesen wurde.

Deswegen ist für so eine Operation die extra Klasse für das Command nicht so wirklich sinnvoll in der Form, wie du das gemacht hast, da du effektiv nicht wirklich an die StatusText-Property herankommst. Besser wäre das über eine Command-Basisklasse mit Actions (siehe meinen Code in Post #2) oder du müsstest dein Command so manipulieren, dass du z. B. das ViewModel selbst mit reinreichst und dann auf dessen StatusText-Property arbeitest. Aber diese Art von Verknüpfung ist nicht gerade guter Stil.

Ziel (und damit wäre dein Beispiel funktional) wäre daher aus meiner Sicht sowas wie:
C#:
public MyCommand ShowCommand {get; set;}
...
// ShowCommand erzeugen, hier mit Lambda für Execute und CanExecute
ShowCommand = new MyCommand(() => StatusText = "funzt", () => true);
...
public string StatusText{
    get => _statusText;
    set {
        if(_statusText == value)
            return;
        _statusText = value;
        OnPropertyChanged(nameof(StatusText));
    }
}
 
  • Gefällt mir
Reaktionen: marcOcram
Jetzt bin ich gerade ganz raus. Also heißt das, es gibt keine saubere Methode um mit meinem Code zu einem Ziel zu kommen, welcher sauberen Code erzeugt?

Bei deinem Code weiß ich nicht was bei den ... für Code kommen soll. Langsam komme ich immer weiter ab von der Sache.
 
Ich hab mal versucht dein aktuelles Konzept aufzuzeichnen:

IMG_0233.PNG


Du hast das MainWindow, welches deine UI-Elemente enthält und die Bindings spezifiziert. Dem MainWindow hast du einen DataContext zugewiesen, dein MainWindowViewModel.
Dieses definiert das Property StatusText (mit INotifyPropertyChanged).

Jetzt kommen wir zum eigentlichen Problem. Wie bei jeder Klasse wird eine Instanz erzeugt, die einmalig ist. Du kannst theoretisch unendlich Instanzen deines ViewModels erzeugen, aber nur die eine im DataContext wird für die Bindings herangezogen und vom MainWindow beobachtet.
In deinem ShowCommand erzeugst du eine neue Instanz der ViewModel-Klasse, die nichts mit dem DataContext zu tun hat. Du kannst da alles mögliche reinschreiben, es wird nie in der UI ankommen. Um das zu untersuchen kannst du einfach mal einen Breakpoint in den Setter deiner Property StatusText sowie in deine Execute-Methode vom ShowCommand stellen und du wirst sehen, dass du den einen StatusText veränderst, der andere aber nicht ausgelöst wird...sie kennen sich halt nicht.

Daher bedarf es hier einer anderen Lösung. Dazu zwei Ansätze:
  1. Der Konstruktor deines ShowCommands bekommt einen Parameter vom Typ MainWindowViewModel übergeben. Das heißt du übergibst die Instanz der Klasse MainWindowViewModel (die den DataContext darstellt) an dein ShowCommand und speicherst diese in einem Feld des ShowCommands: private MainWindowViewModel viewModel;. Dann sagst du in der Execute-Methode viewModel.StatusText = "funzt" und erzeugst KEINE neue Instanz.
    Das ist zwar eine Möglichkeit deinen Aufbau zum laufen zu bekommen, aber sehr schlechter Coding Style! Daher nutze das gerne um zu sehen, dass es funktioniert, aber gewöhne dir das bitte nicht an.
  2. Du erschaffst ein Kommando, dass generisch Aktionen entgegennehmen kann und diese ausführt. Dadurch wird dein Kommando vom ViewModel entkoppelt und führt lediglich das aus, was ihm von außen herein gereicht wird (siehe Code Post 2, wenn du Actions und Func's nicht kennst/vestehst bitte nachschlagen und ggf. rückfragen). Die eigentliche Logik zur Veränderung der Property StatusText erfolgt dann im ViewModel selbst, also der Instanz, die auch den DataContext darstellt!
Ansatz 2 im schon gezeigten Code, nur noch mal erweitert mit hoffentlich helfenden Kommentaren:
C#:
// Das hier wird einfach direkt im ViewModel definiert, ist also quasi eine neue Property wie schon StatusText
public MyCommand ShowCommand {get; set;}


// Das Kommando wird z. B. im Konstruktor des ViewModels erzeugt
ShowCommand = new MyCommand(ExecuteShow, CanExecuteShow);


// Die übergebenen Methoden werden im ViewModel privat implementiert

// Die CanExecute Methode prüft, ob das Kommando ausgeführt werden darf
private bool CanExecuteShow()
{
    // keine Einschränkung, ShowCommand darf immer ausgeführt werden
    return true;
}

// Die Execute Methode führt die eigentliche Logik aus
private void ExecuteShow()
{
    StatusText = "funzt";
}

Damit hast du zwei Ansätze, die du probieren kannst. Wesentlich einfacher kann ichs nicht erklären. Daher bitte sehr detailliert nachfragen, wenn noch etwas unklar ist, oder recherchieren, wenn bestimmte Begriffe und Zusammenhänge nicht ersichtlich sind. Gerade sowas wie Instanzen von Klassen ist nötiges Basiswissen.

Hoffe ich konnte dir das damit etwas deutlicher erklären :)
 
  • Gefällt mir
Reaktionen: marcOcram
Danke erstmals für dein Verständnis und Geduld mit mir. Also gibt es für mein Beispiel aus Post#4 keinen vernünftigen Weg?

Wie das ganze mit dem eigenen Commando funktioniert verstehe ich auf jeden Fall erstmal. Allerdings hab ich das Gefühl, das der Weg, den du mir aufzeigst, schon der perfekte ist, aber für mich als Anfänger vlt. zu komplex.

Wie wäre denn der einfachere Weg um im guten Style mein Beispiel aus dem 1. Post so zu verändern, dass es hinhaut.

Richtig verstanden habe ich aber, das ich den Datacontext als neue Instanz des MainWindowViewModel instanzieren muss.
So in der Code Behind XAML Datei:

XML:
DataContext = new MainWindowViewModel();

Hab aus Post#2 den Quelltext in meine MyCommand eingefügt.

Folgendes habe ich in meiner MainWindowsViewModel Klasse gemacht. Hab mal alles mit Kommentaren vermerkt


C#:
// INotifyPropertyChanged implementiert
    public class MainWindowViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        // Methode OnPropertyChanged angelegt
        private void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

            // Property angelegt und mit get und set gesetzt
            string _statusText;

        public string StatusText
        {
            get { return _statusText; }
            set
            {
                _statusText = value;
                OnPropertyChanged(StatusText);
            }
        }


        // neues Command definiert
        public MyCommand CopyCommand { get; set; }

        private void Init()
        {
            CopyCommand = new MyCommand(Copy, CanCopy);
        }


        // Kopierlogik einfügen
        private void Copy()
        {
            StatusText = "funzt";

            //Wie komme ich hier an den Text der Texbox?
            Clipboard.SetText("Woher kommt der Text aus der Textbox?");
        }

        private bool CanCopy()
        {
            return true;
        }
    }

}

Jetzt ist allerdings das Problem das mein Command gar nicht mehr gefeuert wird.

Führen meinen Aufruf in der XAML so auf:
XML:
<Button Command="{Binding Path=CopyCommand}" CommandParameter="{Binding Text, ElementName=tb_ShowID}">

<TextBlock Name="tb_ShowID" />
 
Deine Methode Init() wird nirgendwo aufgerufen, somit ist der Wert von CopyCommand immer null.

C#:
    // INotifyPropertyChanged implementiert
    public class MainWindowViewModel : INotifyPropertyChanged
    {
        public MainWindowViewModel()
        {
            // Init aufrufen!
            Init();
        }
   
        public event PropertyChangedEventHandler PropertyChanged;

        // Methode OnPropertyChanged angelegt
        private void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        // Property angelegt und mit get und set gesetzt
        string _statusText;

        public string StatusText
        {
            get { return _statusText; }
            set
            {
                _statusText = value;
                OnPropertyChanged(nameof(StatusText)); // hier hat das nameof() gefehlt
            }
        }

        // neues Command definiert
        public MyCommand CopyCommand { get; set; }

        private void Init()
        {
            CopyCommand = new MyCommand(Copy, CanCopy);
        }

        // Kopierlogik einfügen
        private void Copy()
        {
            StatusText = "funzt";

            //Wie komme ich hier an den Text der Texbox?
            Clipboard.SetText(TextBoxText);
        }

        private bool CanCopy()
        {
            return true;
        }
       
        // Aktueller Text der TextBox, wird nur aktualisiert wenn der Nutzer die TextBox verlässt (z. B. einen Button drückt).
        public string TextBoxText { get; set; }
    }
}

XML:
<Button Command="{Binding Path=CopyCommand}">

<TextBlock Name="tb_ShowID" Text="{Binding Path=TextBoxText}" />

Es funktioniert auch als Parameter, dafür muss die Klasse MyCommand aber angepasst werden.
Das würde ich jetzt erstmal weglassen außer du weißt wie Generische Methoden und Klassen geschrieben werden und welchen Zweck Sie verfolgen -> erstmal ein Problem beheben, dann ein anderes lösen.
 
Danke für die Antwort, jetzt funktioniert es mit dem Text in der Statusleiste setzen.
Jetzt ist natürlich die Frage, wie komme ich nun an den Text aus der TextBox heran um ihn in die Zwischenablage zu kopieren?
 
Nero1 schrieb:
Hat er doch in seiner Copy-Methode bereits eingepflegt?
Jetzt bin ich ganz raus?

XML:
// Kopierlogik einfügen
        private void Copy()
        {
            StatusText = "funzt";

            //Wie komme ich hier an den Text der Texbox?
            Clipboard.SetText("Woher kommt der Text aus der Textbox?");
        }

Ich möchte ja nicht den String kopieren, sondern den Text welcher in einer Textbox steht. Zustätzlich soll der Status Text auf "funzt" gesetzt werden.
 
XML:
<TextBlock Name="tb_ShowID" Text="{Binding Path=TextBoxText}" />
C#:
// Kopierlogik einfügen
private void Copy()
{
    StatusText = "funzt";

    // Textbox-Text aus dem Binding in die Zwischenablage packen
    Clipboard.SetText(TextBoxText);
}

Du machst das genauso wie beim StatusText. Binding-Property erstellen, die in ne Textbox binden (is von ihm halt als Textblock gemacht aber is ja auch egal, tauscht du das Element eben aus) und im Copy-Command auslesen.

Hinweis: Textboxen aktualisieren per default erst bei Fokusverlust, das heißt wenn du aus der Textbox raus gehst, woanders hin klickst usw. Willst du immer den aktuellen Wert haben muss das Binding angepasst werden zu {Binding TextBoxText, UpdateSourceTrigger=PropertyChanged} (statt UpdateSourceTrigger=LostFocus, das wäre das Standardverhalten).
 
Da bekomme ich aber nur einen String mit null zurück.

XML:
<TextBlock  Name="tb_showID" Text="{Binding Path=TextBoxText, UpdateSourceTrigger=PropertyChanged}"/>

C#:
 public string TextBoxText
        {
            get { return _TextBoxText; }
            set
            {
                _TextBoxText = value;
                OnPropertyChanged(nameof(TextBoxText));
            }
            }


C#:
private void Copy()
        {
            StatusText = "funzt";
            //Wie komme ich hier an den Text der Texbox?
            Clipboard.SetText(TextBoxText);
        }
 
Nero1 schrieb:
(is von ihm halt als Textblock gemacht aber is ja auch egal, tauscht du das Element eben aus)
Muss natürlich auch eine TextBox sein, ich hab den Nebensatz ja nicht umsonst geschrieben. Musst du logischerweise austauschen, wenn du Text aus einer TextBox auslesen willst. In deinem TextBlock steht sonst ja auch nix drin.
 
Irgendwie reden wir aneinander vorbei.
Meine XAML sieht so aus.

XML:
<StackPanel Grid.Column="0" Grid.Row="1" >
            <Label Content="Product-Key" />
            
                <Button Command="{Binding CopyCommand}" CommandParameter="{Binding Text, ElementName=tb_ShowID}" />
                    <TextBlock  Name="tb_ShowID" Text="{Binding Path=TextBlockText, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>

Und meine MainWindowViewModel sieht so aus:

C#:
 public class MainWindowViewModel : INotifyPropertyChanged
    {
        public MainWindowViewModel()
        {
           Init();
        }

        public event PropertyChangedEventHandler PropertyChanged;

        // Methode OnPropertyChanged angelegt
        private void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
        // Property angelegt und mit get und set gesetzt
        string _statusText;
        string _TextBlockText;
        public string StatusText
        {
            get { return _statusText; }
            set
            {
                _statusText = value;
                OnPropertyChanged(nameof(StatusText));
            }
        }
        // Hier lese ich den Text aus Textblock?
        public string TextBlockText
        {
            get { return _TextBlockText; }
            set
            {
                _TextBlockText = value;
                OnPropertyChanged(nameof(TextBlockText));
            }
        }
        // neues Command definiert
        public MyCommand CopyCommand { get; set; }

        private void Init()
        {
            CopyCommand = new MyCommand(Copy, CanCopy);
        }

        // Kopierlogig einfügen
        private void Copy()
        {
            StatusText = "funzt";
            //Und hier bekomme ich immer null zurück
            Clipboard.SetText(TextBlockText);
        }

        private bool CanCopy()
        {
            return true;
        }

    }
 
Sieht genau so aus wie erwartet. Les dich mal ein was der Unterschied zwischen einem TextBlock und einer TextBox ist.
Und wenn du das getan hast und dich immer noch wunderst, warum du nichts kopieren kannst, dann schau mal was in der UI in deinem TextBlock steht und versuche da mal was reinzuschreiben. Ich denke dann kommst du von selbst drauf, warum du keinen TextBlock dafür verwenden kannst :) Du hast nämlich gar keine Möglichkeit per UI da irgendwas reinzuschreiben, was du ja allem Anschein nach vorhast.
 
Dass ich einen Textblock nur im XAML bzw. im CodeBehind etwas mitgeben kann, ist mir bewusst. Daher setze ich in der Code Behind Datei den Text des Textblockes. Später soll dieser via Methode einen Wert bekommen.
C#:
tb_ShowID.Text = "Keine ID gefunden!";

Im 1. Post hatte ich
XML:
<Button Command="{Binding Path=ShowCommand}" CommandParameter="{Binding Text, ElementName=tb_ShowID}">
<TextBlock Name="tb_ShowID" />
und damit hat das kopieren ja funktioniert.


Edit: Vielleicht zu bessere Verständnis, habe ich mal einen Screen angehängt.
  • Die ID (Textbox soll per Methode gefüllt werden).
  • Über Button soll der Text der ID in die Zwischenablage kopiert werden
  • am Anschluss soll in der StatusZeile: "kopiert" ausgegeben werden.
 

Anhänge

  • screen.jpg
    screen.jpg
    11,1 KB · Aufrufe: 248
Zuletzt bearbeitet:
Na dann erzeuge in deinem ViewModel einfach mal ne Methode GenerateRandomId(), die dir eine zufällige ID generiert und schreibe diese auf TextBlockText. Die packst du in den Konstruktor und spaßeshalber rufst du sie auch im Execute deines Button-Commands auf. Dann solltest du (wenn alles richtig war und ich nix übersehen habe) bei jedem Klick die aktuelle ID kopiert haben und gleichzeitig wird eine neue erzeugt, wo du dann ein paar Mal kopieren kannst und jedes Mal die entsprechende, unterschiedliche ID in der Zwischenablage liegen sollte.

Also Methode -> generiert ID -> schreibt die ID auf die Property des TextBlocks -> Button drücken -> schreibt StatusText, liest Property aus und kopiert diesen Wert in die Zwischenablage.
 
Aber ich befülle die Textbox doch in der MainWindow Code Behind Datei schon mit einem String
C#:
tb_ShowID.Text = "Keine ID gefunden!";
Dieser wird ja auch beim Start angezeigt.

Langsam aber sicher verliere ich immer mehr den Faden.
 
Zurück
Oben