Blog Dla Młodszych Programistów C#/.NET

W poprzedniej części zaczęliśmy implementować aplikację w WPF. Stworzyliśmy wtedy widok główny naszego kalkulatora w XAML'u. Przygotowaliśmy również naszą aplikację, tak aby można było trzymać się najlepszych praktyk i zastosować wzorzec MVVM. W tym artykule dokończymy całą aplikację – zaimplementujemy całą logikę kalkulatora. Tym razem jednak zrobimy to trochę inaczej niż wcześniej w aplikacji Windows Forms. Stworzymy bardziej uniwersalne rozwiązanie. Jeżeli nie widziałeś jeszcze poprzedniego materiału, gdzie tworzyliśmy widok naszej aplikacji, to koniecznie zrób to, zanim przejdziesz do tego artykułu.

Pierwsza Aplikacja Desktopowa WPF w C# – Logika MVVM (2/2)


Implementacja


Jeżeli rozwijasz aplikację razem ze mną, to otwórz proszę projekt, który utworzyliśmy w poprzednim tygodniu i możemy przejść do całej implementacji.

Dla przypomnienia tak wygląda widok, który stworzyliśmy w poprzednim artykule:

PIERWSZA APLIKACJA Desktopowa WPF MVVM Logika - widok


ViewModel


Logika naszego kalkulatora będzie w ViewModelu, dlatego dodajmy najpierw nową klasę we wcześniej utworzonym folderze ViewModels, w którym będzie ten ViewModel. Ta klasa może mieć dowolną nazwę, niech to będzie MainViewModel. Nasz każdy ViewModel powinien implementować interfejs INotifyPropertyChanged.

using System.ComponentModel;

namespace Calculator.WpfApp.ViewModels
{
    public class MainViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
    }
}

Nie zapomnij na górze dodać odpowiedniego using'a, to znaczy System.ComponentModel. Dodałem także deklaracje zdarzenia PropertyChanged, które powinno być wywołany wtedy, gdy zmieni się jakaś właściwość. Najlepiej od razu dodać sobie taką dodatkową metodę (najczęściej o nazwie OnPropertyChanged), w której będziemy wywoływać to zdarzenie, zaoszczędzi nam to trochę czasu, bo będzie ona wywoływana w wielu miejscach. Najczęściej będziemy tę metody wywoływać we właściwościach, a konkretnie w set'ach, w celu zaktualizowania widoku.

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace Calculator.WpfApp.ViewModels
{
    public class MainViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Do metody OnPropertyChanged przekazujemy tylko nazwę właściwości (propertyName), która powinna zostać odświeżona, domyślnie jest to null. Dodatkowo ten parametr został oznaczony atrybutem CallerMemberName, dzięki któremu domyślnie zostanie do tej metody przekazana nazwa właściwości, w której metoda zostanie wywołana. Czyli jeżeli wywołamy metodę w set właściwości o nazwie np. FirstName, bez przekazania parametru, to do metody i tak zostanie przekazany FirstName. Także będzie trochę mniej kodu do napisania.

Wewnątrz metody za pomocą Invoke wyzwalamy event PropertyChanged, jeżeli nie jest null'em. Tak może wyglądać ta metoda i za chwilę pokażę Ci, jak będziemy jej używać. Dodamy za chwilę kilka właściwości, gdzie wykorzystamy metodę OnPropertyChanged.


Powiązanie widoku z ViewModelem


Na początek jednak, żebyśmy nie zapomnieli, musimy powiązać ten ViewModel z widokiem. Tak naprawdę, możemy to zrobić na kilka sposobów. Możemy to zrobić w widoku w XAMLu, gdzie wystarczy ustawić DataContext, ale myślę, że łatwiejszym i szybszym sposobem jest po prostu wpisanie 1 linjki kodu w CodeBehind, czyli w pliku MainWindow.xaml.cs

DataContext = new MainViewModel();

Tak będzie wyglądała cała klasa MainWindows.xaml.cs:

using Calculator.WpfApp.ViewModels;
using MahApps.Metro.Controls;

namespace Calculator.WpfApp
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : MetroWindow
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new MainViewModel();
        }
    }
}

Takim zapisem wskazujemy, że dla tego okna MainWindow – ViewModelem jest klasa MainViewModel.


RelayCommand


Aby móc powiązać również zdarzenia z widoków z metodami z ViewModelu potrzebujemy jeszcze jednej pomocniczej klasy. Dodajmy sobie najpierw nowy folder Commands, a w nim dodamy klasę RelayCommand. Ta nazwa jest oczywiście dowolna. Ważne, żeby implementowana ona interfejs ICommand.

using System;
using System.Windows.Input;

namespace Calculator.WpfApp.Commands
{
    public class RelayCommand : ICommand
    {
        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter)
        {
            throw new NotImplementedException();
        }

        public void Execute(object parameter)
        {
            throw new NotImplementedException();
        }
    }
}

Musimy teraz zaimplementować takie 2 metody. Oczywiście, nie trzeba tego implementować za każdym od zera, wystarczy raz zaimplementować, a później wykorzystywać w innych projektach. Także skopiuje sobie implementacje z innego projektu i opiszę Ci, jak to dokładnie działa.

using System;
using System.Windows.Input;

namespace Calculator.WpfApp.Commands
{
    public class RelayCommand : ICommand
    {
        readonly Action<object> _execute;
        readonly Predicate<object> _canExecute;

        public RelayCommand(Action<object> execute)
            : this(execute, null)
        {
        }

        public RelayCommand(Action<object> execute, Predicate<object> canExecute)
        {
            _execute = execute ?? throw new ArgumentNullException("execute");
            _canExecute = canExecute;
        }

        public bool CanExecute(object parameter)
        {
            return _canExecute == null || _canExecute(parameter);
        }

        public event EventHandler CanExecuteChanged
        {
            add
            {
                CommandManager.RequerySuggested += value;
            }
            remove
            {
                CommandManager.RequerySuggested -= value;
            }
        }

        public void Execute(object parameter)
        {
            _execute(parameter);
        }
    }
}

Czyli mamy dwa delegaty. 1 to jest Action, który będzie przyjmował parametr object, a drugi Predicate, który również może przyjmować parametr object i zwraca boola.

readonly Action<object> _execute;
readonly Predicate<object> _canExecute;

Poniżej mamy dwa konstruktory. Jeżeli będziemy tworzyć nowy obiekt tej klasy, to jako parametr konstruktora możemy przekazać dwie metody. Jedna metoda, która ma się wykonać po wyzwoleniu jakiegoś zdarzenia, a druga mówi nam o tym, czy ta metoda może się wykonać.

public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
    _execute = execute ?? throw new ArgumentNullException("execute");
    _canExecute = canExecute;
}

Czyli jeżeli przekażemy jakąś metodę, która zwraca false, to ta akcja się nie wykona.

Możesz też utworzyć nowy obiekt, tylko przekazując metodę, która ma zostać powiązana z tą konkretną akcją i wtedy jako drugi parametr zostanie przekazany null.

public RelayCommand(Action<object> execute)
    : this(execute, null)
{
}

Poniżej mamy metodę CanExecute, która jeżeli CanExecute będzie null'em lub będzie zwracała true, to wtedy cała metoda CanExecute zwraca true. Dalej mamy jeszcze metodę Execute, która po prostu wywołuje przekazaną wcześniej metodę. Także, tak może wyglądać przykładowa implementacja klasy RelayCommand. Pokażę Ci teraz, jak wygląda właśnie to wiązanie metod i właściwości.


Binding właściwości


Przejdź proszę do widoku głównego, czyli MainWindow.xaml. Mamy tutaj na górze zwykłego TextBox'a. W TextBox'ie możemy wyświetlić, czy też wpisać dowolny tekst. Możemy to zrobić na różne sposoby, to znaczy po pierwsze możemy np. uzupełnić właściwość Text.

<TextBox Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="5" Text="123" />

To jest najprostszy sposób, ale też najrzadziej wykorzystywany, ponieważ mamy tutaj przypisany tekst na stałe. Może to być przydane tylko w niektórych przypadkach. Zazwyczaj jednak ten tekst chcemy zmieniać podczas działania programu. Trochę lepszy rozwiązaniem będzie nadanie nazwy tej kontrolce i później odnoszenie się do niej w CodeBehind. Czyli wtedy tak by to wyglądało:

<TextBox Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="5" Name="tbScreen" />

A w CodeBehind:

tbScreen.Text = "123";

To wszystko jak najbardziej zadziała. Tylko my chcemy, aby nasza aplikacja korzystała ze wzorca MVVM i nie chcemy tutaj za bardzo wpisywać żadnego kodu w CodeBehind. Całą logikę chcemy mieć w naszym ViewModelu. Także, aby to zrobić prawidłowo, musimy powiązać te dane z odpowiednimi właściwościami z MainViewModel. Wygląda to tak, w widoku wiążemy te dane za pomocą Binding:

<TextBox Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="5" Text="{Binding ScreenVal}" />

Czyli mówimy tutaj, że chcemy powiązać właściwość Text tej konkretnej kontrolki TextBox z właściwością o nazwie ScreenVal, którą zaraz utworzymy w naszym ViewModelu, który z kolei powiązany jest z tym widokiem.

Tworzymy w MainViewModel wraz z polem taką właściwość:

private string _screenVal;
public string ScreenVal
{
    get { return _screenVal; }
    set 
    { 
        _screenVal = value;
        OnPropertyChanged();
    }
}

Ta właściwość powinna być string'iem (tak samo jako właściwość Text TextBox'a). Tak jak Ci wspominałem wcześniej, w set jeszcze musimy wywołać naszą wcześniej stworzoną metodę OnPropertyChanged, tak żeby po zmianie w ViewModelu odświeżyły się dane w widoku. Bez wywołania tej metody, widok nie zostałby zaktualizowany. Teraz, jeżeli zmienimy wartość tej właściwości, możemy to np. zrobić w konstruktorze, to widok zostanie zaktualizowany.

public MainViewModel()
{
    ScreenVal = "0";
}

Tak będzie wyglądać aplikacja po uruchomieniu:

PIERWSZA APLIKACJA Desktopowa WPF MVVM Logika - TextBox

Jak widzisz, widok został zaktualizowany. Wszystko działa, tak jak powinno. Czyli mamy w widoku TextBox, którego właściwość Text jest powiązana z właściwością ScreenValue z ViewModelu. Następnie w ViewModelu mamy taką właściwość i teraz, jeżeli będziemy zmieniać wartość tej właściwości, to automatycznie będzie to wszystko wyświetlane na widoku. Dzięki temu, że w set właśnie mamy OnPropertyChanged, czyli ten event PropertyChanged tutaj jest wyzwalany, to widok tak jakby "widzi", że to się zmieniło w ViewModelu i każda zmiana będzie od razu wyświetlana na widoku. Tak wygląda wiązanie z właściwościami.


Binding metod


Oprócz tego jeszcze chcemy oczywiście powiązać nasze zdarzenia z metodami. Do tego użyjemy komend (Command) i ponownie wiązania (Binding). Przejdźmy ponownie do widoku MainWindow i dodaj wiązanie do komendy na początek dla przycisku 7, tak to może wyglądać:

<Button Grid.Row="1" Content="7" Command="{Binding AddNumberCommand}" />

Mówimy w tym miejscu, że chcemy powiązać zdarzenie Click dla naszego przycisku z komendą AddNumberCommand, która zostanie zaraz stworzona w ViewModelu. Jeżeli przycisk zostanie kliknięty, to zostanie wywołana odpowiednia metoda.

Przejdź ponownie do MainViewModel. Dodamy tutaj wymaganą komendę, która również będzie właściwością:

public ICommand AddNumberCommand { get; set; }

Następnie musimy zainicjalizować tą komendę w konstruktorze:

public MainViewModel()
{
    ScreenVal = "0";
    AddNumberCommand = new RelayCommand(AddNumber);
}

Tutaj tworzymy instancje klasy RelayCommand, którą przed chwilą stworzyliśmy. Nie zapomnij dodać na górze odpowiedniego using'a System.Windows.Input, a także: Calculator.WpfApp.Commands. Zauważ, że jako parametr do konstruktora RelayCommand przekazaliśmy metodę o nazwie AddNumber, także potrzebujemy stworzyć taką metodę.

private void AddNumber(object obj)
{
}

Czyli dokładnie ta metoda zostanie wywołana, gdy zostanie kliknięty przycisk 7. Możemy to sprawdzić. W tym celu możesz wyświetlić w metodzie AddNumber odpowiedni komunikat:

private void AddNumber(object obj)
{
    MessageBox.Show("AddNumber Clicked");
}

Sprawdź teraz, czy taki komunikat u Ciebie się pojawia.

PIERWSZA APLIKACJA Desktopowa WPF MVVM Logika - Button Clicked

U mnie wszystko na tę chwilę działa prawidłowo. Za chwilę tutaj stworzymy już faktyczną logikę.

Czyli jeszcze raz wytłumaczę Ci jak działy cały ten mechanizm. Mamy w widoku przycisk, na tę chwilę podpięliśmy się tylko pod przycisk z 7 i wiążemy tutaj jego zdarzenie Click z komendą AddNumberCommand, która jest w ViewModelu. Komenda o takiej nazwie musi być właściwością w ViewModelu. Następnie np. w konstruktorze inicjalizujemy tę komendę i mówimy, że chcemy, żeby po kliknięciu została wywołana metoda AddNumber, która wyświetli nam w tej chwili taki testowy komunikat.


Przekazanie parametru do ViewModelu


Myślę, że możemy przejść dalej. Także, tak samo będziemy chcieli podpiąć tę komendę i metodę pod inne przyciski, ale też właśnie musimy jakoś rozróżniać, żebyśmy wiedzieli który przycisk został kliknięty. Także oprócz tego, że mamy powiązane komendę, to chcemy w widoku jeszcze przekazać parametr. W tym celu musimy do CommandParameter przypisać, to co chcemy przekazać, czyli w tym przypadku będzie to 7.

<Button Grid.Row="1" Content="7" Command="{Binding AddNumberCommand}" CommandParameter="7" />

Dzięki temu do metody AddNumber jako parametr zostanie przekazana ta 7.

private void AddNumber(object obj/*7*/)
{
    MessageBox.Show("AddNumber Clicked");
}

Możemy też to przetestować. W metodzie jako parametr dostajemy object, także musimy go rzutować na string'a i spróbujemy to wyświetlić na ekranie.

private void AddNumber(object obj)
{
    MessageBox.Show(obj as string);
}

Po kliknięciu 7:

PIERWSZA APLIKACJA Desktopowa WPF MVVM Logika - kliknięta 7 parametr


Pozostałe wiązanie danych


Pozostaje nam jeszcze powiązanie reszty przycisków, tak żeby wszędzie to tak działało, czyli to samo będzie dla 0, 1, 2, 3, 4, 5, 6, 7, 8 i 9, a także dla ",".

<Button Grid.Row="1" Content="7" Command="{Binding AddNumberCommand}" CommandParameter="7" />

<Button Grid.Row="1" Grid.Column="1" Content="8" Command="{Binding AddNumberCommand}" CommandParameter="8" />

<Button Grid.Row="1" Grid.Column="2" Content="9" Command="{Binding AddNumberCommand}" CommandParameter="9" />

<Button Grid.Row="2" Content="4" Command="{Binding AddNumberCommand}" CommandParameter="4" />

<Button Grid.Row="2" Grid.Column="1" Content="5" Command="{Binding AddNumberCommand}" CommandParameter="5" />

<Button Grid.Row="2" Grid.Column="2" Content="6" Command="{Binding AddNumberCommand}" CommandParameter="6" />

<Button Grid.Row="3" Content="1" Command="{Binding AddNumberCommand}" CommandParameter="1" />

<Button Grid.Row="3" Grid.Column="1" Content="2" Command="{Binding AddNumberCommand}" CommandParameter="2" />

<Button Grid.Row="3" Grid.Column="2" Content="3" Command="{Binding AddNumberCommand}" CommandParameter="3" />

<Button Grid.Row="4" Grid.ColumnSpan="2" Content="0" Command="{Binding AddNumberCommand}" CommandParameter="0" />

<Button Grid.Row="4" Grid.Column="2" Content="," Command="{Binding AddNumberCommand}" CommandParameter="," />

W każdym przypadku do ViewModelu zostanie przekazany odpowiedni parametr i na tej podstawie rozróżnimy, który przycisk został kliknięty.

Trochę inna logika będzie dla naszych operacji, może sobie od razu powiązać przyciski operacji z kolejną komendą o nazwie AddOperationCommand.

<Button Grid.Row="4" Grid.Column="2" Content="," Command="{Binding AddNumberCommand}" CommandParameter="," />

<Button Grid.Row="1" Grid.Column="3" Content="/" Command="{Binding AddOperationCommand}" CommandParameter="/" />

<Button Grid.Row="2" Grid.Column="3" Content="-" Command="{Binding AddOperationCommand}" CommandParameter="-" />

<Button Grid.Row="3" Grid.Column="3" Content="*" Command="{Binding AddOperationCommand}" CommandParameter="*" />

<Button Grid.Row="1" Grid.Column="4" Grid.RowSpan="2" Content="+" Command="{Binding AddOperationCommand}" CommandParameter="+" />

Tak samo, jak wcześniej, również tutaj przekażemy odpowiedni parametr. Następnie w ViewModelu dodamy nową właściwość i zainicjalizujemy ją w konstruktorze.

public ICommand AddOperationCommand { get; set; }

public MainViewModel()
{
    ScreenVal = "0";
    AddNumberCommand = new RelayCommand(AddNumber);
    AddOperationCommand = new RelayCommand(AddOperation);
}

W konstruktorze przekażemy metodę AddOperation, którą zaraz stworzymy.

private void AddOperation(object obj)
{
}

Potrzebujemy jeszcze osobną komendę do przycisku Clear, także w widoku wpisujemy:

<Button Grid.Row="4" Grid.Column="3" Content="C" Command="{Binding ClearScreenCommand}" />

Dodajemy taką właściwość w ViewModelu, inicjalizujemy w konstruktorze.

public ICommand ClearScreenCommand { get; set; }

public MainViewModel()
{
    ScreenVal = "0";
    AddNumberCommand = new RelayCommand(AddNumber);
    AddOperationCommand = new RelayCommand(AddOperation);
    ClearScreenCommand = new RelayCommand(ClearScreen);
}

Tworzymy metodę, która ma zostać wywołana.

private void ClearScreen(object obj)
{
}

I tak samo zrobimy dla przycisku, który będzie wyświetlał wynik, w tym przypadku powiążemy przycisk z komendą GetResultCommand.

<Button Grid.Row="3" Grid.Column="4" Grid.RowSpan="2" Content="=" Command="{Binding GetResultCommand}" />

Nowa komenda w ViewModelu, inicjalizacja w konstruktorze i dodajemy nową metodę GetResult.

public ICommand GetResultCommand { get; set; }

public MainViewModel()
{
    ScreenVal = "0";
    AddNumberCommand = new RelayCommand(AddNumber);
    AddOperationCommand = new RelayCommand(AddOperation);
    ClearScreenCommand = new RelayCommand(ClearScreen);
    GetResultCommand = new RelayCommand(GetResult);
}

private void GetResult(object obj)
{
}


Logika w ViewModelu


Mamy już teraz powiązane wszystkie właściwości i wszystkie zdarzenia w naszym widoku, także teraz tylko musimy uzupełnić ich logikę w ViewModelu i wtedy nasza aplikacja będzie kompletna.

Zacznijmy od metody AddNumber. Zrobimy tutaj podobnie, jak to robiliśmy wcześniej w aplikacji Windows Forms. Najpierw interesuje nas to jaki został przekazany parametr do tej metody, także przypiszemy go do zmiennej number.

var number = obj as string;

Następnie również podobnie jak robiliśmy wcześniej w aplikacji Windows Forms, sprawdzimy najpierw, czy obecnie na tym naszym ekranie jest wyświetlane 0 i liczba, która została przekazana, jest różna od przecinka. Jeżeli tak, to ScreenVal ustawiamy na string empty. W przeciwnym przypadku musimy sprawdzić, czy został przekazany przecinek i wcześniej została wybrana operacja, jeżeli tak, to musimy ten nasz przecinek przekazać razem z zerem.

if (ScreenVal == "0" && number != ",")
    ScreenVal = string.Empty;
else if (number == "," && _availableOperations.Contains(ScreenVal.Substring(ScreenVal.Length - 1)))
    number = "0,";

A tak wygląda lista operacji:

private List<string> _availableOperations = new List<string> { "+", "-", "/", "*" };

Także mamy nowe pole, jest to lista stringów z dostępnymi operacjami. Od razu zostało zainicjalizowane, czyli mamy 4 dostępne operację. Są to: dodawanie, odejmowanie, dzielenie i mnożenie. Zweryfikowaliśmy także, czy ostatnim znakiem była tutaj operacja. Jeżeli tak, to chcemy przekazać przed przecinkiem jeszcze 0, tak żeby później nie było jakichś dziwnych błędów. Na koniec, to co zostało wpisane, wyświetlamy na ekran. Doklejamy to do tych znaków, które były już tam wcześniej.

ScreenVal += number;

Tak wygląda na tę chwilę cała metoda:

private void AddNumber(object obj)
{
    var number = obj as string;

    if (ScreenVal == "0" && number != ",")
        ScreenVal = string.Empty;
    else if (number == "," && _availableOperations.Contains(ScreenVal.Substring(ScreenVal.Length - 1)))
        number = "0,";

    ScreenVal += number;
}

Możemy teraz zobaczyć czy to działa.

PIERWSZA APLIKACJA Desktopowa WPF MVVM Logika - TextBox Testy 1

PIERWSZA APLIKACJA Desktopowa WPF MVVM Logika - TextBox Testy 2

PIERWSZA APLIKACJA Desktopowa WPF MVVM Logika - TextBox Testy 3

Także tutaj już się wszystko ładnie wyświetla. Jeżeli wpiszemy cyfrę, to wyświetla się prawidłowo, jeżeli wpiszemy jako pierwszy znak przecinek, to również jest ok. Także możemy już dodawać cyfry i przecinek.

Potrzebujemy również dodać operację, czyli zaimplementować metodę AddOperation. Tak samo najpierw musimy sprawdzić, co tutaj w ogóle zostało przekazane, która dokładnie operacja została kliknięta.

var operation = obj as string;

Następnie wyświetlamy ją na ekranie.

ScreenVal += operation;

Tak będzie wyglądać metoda AddOperation:

private void AddOperation(object obj)
{
    var operation = obj as string;

    ScreenVal += operation;
}

Sprawdźmy, czy wszystko działa:

PIERWSZA APLIKACJA Desktopowa WPF MVVM Logika - Testy operacji

Wszystko jest ok. Następnie możemy zaimplementować metodę ClearScreen. Tutaj wystarczy do właściwości ScreenVal przypisać 0. Tak może wyglądać ta metoda:

private void ClearScreen(object obj)
{
    ScreenVal = "0";
}

Ok, pozostaje nam jeszcze wyświetlenie wyniku. Tym razem jednak ten wynik będziemy wyświetlać trochę inaczej niż w poprzednich aplikacjach. Możemy to zrobić trochę łatwiejszym sposobem. Jest taka klasa DataTable, która ma metodę Compute, dzięki której możemy obliczyć przekazany ciąg znaków, który jest string'iem. Także stwórzmy sobie najpierw nowe pole:

private DataTable _dataTable = new DataTable();

Trzeba dodać odpowiedni using – System.Data. Teraz użyjemy już metody Compute do naszych obliczeń.

var result = _dataTable.Compute(ScreenVal, "");

Przekazujemy wartość właściwości ScreenVal i przekazujemy filtr, a może to być po prostu pusty string. Przypisujemy to, co zwróci ta metoda do nowej zmiennej result. Na koniec wyświetlamy ten wynik na ekranie:

ScreenVal = result.ToString();

Takie rozwiązanie jest ok, ale jeszcze nie dla wszystkich sytuacji. Musimy jeszcze zabezpieczyć się przed błędami związanymi z nieodpowiednim formatowaniem. To znaczy, przed przekazaniem wartości do metody Compute musimy zastąpić przecinek – kropką:

var result = _dataTable.Compute(ScreenVal.Replace(",", "."), "");

Możemy sobie jeszcze wynik zaokrąglić do 2 miejsc po przecinku. Wystarczy skorzystać z metody Round, statycznej klasy Math.

var result = Math.Round(Convert.ToDouble(_dataTable.Compute(ScreenVal.Replace(",", "."), "")), 2);

I cała metoda może wyglądać w ten sposób:

private void GetResult(object obj)
{
    var result = Math.Round(Convert.ToDouble(_dataTable.Compute(ScreenVal.Replace(",", "."), "")), 2);

    ScreenVal = result.ToString();
}

Teraz można przetestować aplikację, zobaczmy czy to działa. Konieczne jest sprawdzenie różnych przypadków.

PIERWSZA APLIKACJA Desktopowa WPF MVVM Logika - Kalkulator wynik testy

PIERWSZA APLIKACJA Desktopowa WPF MVVM Logika - Kalkulator wyświetlenie wyniku testy

Standardowa ścieżka działa prawidłowo. Jednak trzeba pamiętać, że użytkownicy klikają w najróżniejsze sposoby i trzeba zabezpieczyć się przed wszystkimi przypadkami.

PIERWSZA APLIKACJA Desktopowa WPF MVVM Logika - Kalkulator różne operacjie zabezpieczenie

Jak widzisz użytkownik, zamiast wprowadzić liczbę po wybraniu operacji, wybrał kolejne operacje i w takiej sytuacji nasza aplikacja nie zadziałała prawidłowo. Nie możemy dopuścić do takiej sytuacji. Musimy się zabezpieczyć przed taką sytuacją. Na szczęście nie jest to trudne. Możemy sobie dodać nowe pole:

private bool _isLastSignAnOperation;

Które, jak sama nazwa wskazuje, będzie przechowywała informacje o tym, czy ostatni znak jest operacją. Jeżeli będzie operacją, to nie możemy pozwolić na kliknięcie ponownie na kolejną operację, tylko w takiej sytuacji może zostać wybrana liczba. Musimy w kilku miejscach ustawić wartość tego pola.

Po pierwsze, jeżeli klikniemy numer, to znaczy, że ostatnim znakiem nie jest operacja, także możemy ustawić wtedy to pole na false.

private void AddNumber(object obj)
{
    var number = obj as string;

    if (ScreenVal == "0" && number != ",")
        ScreenVal = string.Empty;
    else if (number == "," && _availableOperations.Contains(ScreenVal.Substring(ScreenVal.Length - 1)))
        number = "0,";

    ScreenVal += number;

    _isLastSignAnOperation = false;
}

Jeżeli zostanie kliknięta operacja, to oczywiście ustawiamy na true.

private void AddOperation(object obj)
{
    var operation = obj as string;

    ScreenVal += operation;

    _isLastSignAnOperation = true;
}

Jeżeli zostanie kliknięty przycisk Clear, to również ustawiamy na false.

private void ClearScreen(object obj)
{
    ScreenVal = "0";

    _isLastSignAnOperation = false;
}

Teraz na podstawie tego pola chcemy blokować również niektóre przyciski. Chcemy blokować przycisk operacji, tak żeby dwie operacje nie zostały kliknięte jedna po drugiej i też chcemy blokować przycisk result, ponieważ nie chcemy, żeby w sytuacji, gdy ostatni znak jest operacja, to był obliczany wynik. Aby to zrobić, wystarczy w konstruktorze dla komend AddOperationCommand oraz GetResultCommand uzupełnić ten drugi parametr konstruktora RelayCommand, o którym wcześniej Ci wspominałem, gdzie zdefiniujemy, kiedy chcemy, żeby ta metoda była dostępna.

AddOperationCommand = new RelayCommand(AddOperation, CanAddOperation);
GetResultCommand = new RelayCommand(GetResult, CanGetResult);

Następnie dodamy implementacje tych metod:

private bool CanGetResult(object obj)
{
    return !_isLastSignAnOperation;
}

private bool CanAddOperation(object obj)
{
    return !_isLastSignAnOperation;
}

Chcemy, żeby te metody mogły zostać wywołane tylko wtedy gdy pole isLastSignAnOperation ma wartość false. Możemy sobie również skrócić wersję tych metod i zapisać ją w ten sposób:

private bool CanGetResult(object obj) => !_isLastSignAnOperation;

private bool CanAddOperation(object obj) => !_isLastSignAnOperation;

Zobaczmy czy to działa.

PIERWSZA APLIKACJA Desktopowa WPF MVVM Logika - Kalkulator zabezpieczenie przed operacjami

Zauważ, że po wybraniu operacji, nie ma już teraz możliwości wybrania kolejnej. Trzeba wybrać liczbę, także nie ma możliwości wybrania kilku operacji jedna po drugiej, ponieważ zostały zablokowane. Zawsze po wybraniu operacji będziemy oczekiwać na podanie liczby i o to chodziło.


Cały kod ViewModelu


using Calculator.WpfApp.Commands;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace Calculator.WpfApp.ViewModels
{
    public class MainViewModel : INotifyPropertyChanged
    {
        private string _screenVal;
        private List<string> _availableOperations = new List<string> { "+", "-", "/", "*" };
        private DataTable _dataTable = new DataTable();
        private bool _isLastSignAnOperation;

        public MainViewModel()
        {
            ScreenVal = "0";
            AddNumberCommand = new RelayCommand(AddNumber);
            AddOperationCommand = new RelayCommand(AddOperation, CanAddOperation);
            ClearScreenCommand = new RelayCommand(ClearScreen);
            GetResultCommand = new RelayCommand(GetResult, CanGetResult);
        }

        private bool CanGetResult(object obj) => !_isLastSignAnOperation;

        private bool CanAddOperation(object obj) => !_isLastSignAnOperation;

        private void GetResult(object obj)
        {
            var result = Math.Round(Convert.ToDouble (_dataTable.Compute(ScreenVal.Replace(",", "."), "")), 2);

            ScreenVal = result.ToString();
        }

        private void ClearScreen(object obj)
        {
            ScreenVal = "0";

            _isLastSignAnOperation = false;
        }

        private void AddOperation(object obj)
        {
            var operation = obj as string;

            ScreenVal += operation;

            _isLastSignAnOperation = true;
        }

        private void AddNumber(object obj)
        {
            var number = obj as string;

            if (ScreenVal == "0" && number != ",")
                ScreenVal = string.Empty;
            else if (number == "," && _availableOperations.Contains(ScreenVal.Substring(ScreenVal.Length - 1)))
                number = "0,";

            ScreenVal += number;

            _isLastSignAnOperation = false;
        }

        public ICommand AddNumberCommand { get; set; }
        public ICommand AddOperationCommand { get; set; }
        public ICommand ClearScreenCommand { get; set; }
        public ICommand GetResultCommand { get; set; }

        public string ScreenVal
        {
            get { return _screenVal; }
            set 
            { 
                _screenVal = value;
                OnPropertyChanged();
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void OnPropertyChanged(
            [CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Rozwiązanie, które pokazałem Ci w tym artykule, jest bardziej uniwersalne, niż rozwiązanie, które implementowaliśmy w windows forms. Możemy tutaj obliczać wiele działań. Chciałem Ci pokazać, że taki kalkulator można napisać na różne sposoby.

PIERWSZA APLIKACJA Desktopowa WPF MVVM Logika - Kalkulator działanie aplikacji

PIERWSZA APLIKACJA Desktopowa WPF MVVM Logika - Kalkulator wyświetlenie wyniku

Zdecydowanie było to chyba prostsze rozwiązanie do zaimplementowania. Warto byłoby to jeszcze troszkę przeklikać i zobaczyć czy we wszystkich przypadkach działa to według założenia, ale wydaje mi się, że już tutaj wszystko powinno być w porządku.


PODSUMOWANIE


Jak widzisz aplikacja w WPF'ie mimo, iż również jest aplikacją desktopową tak samo, jak w Windows Forms, to w obu przypadkach implementacja wygląda inaczej. Pisząc aplikację w WPF'ie, stosowaliśmy wzorzec MVVM, którego znajomość również bardzo przyda Ci się jeszcze w innych typach aplikacji, o czym przekonasz się w kolejnych materiałach. Stosując ten wzorzec prawidłowo, twoja aplikacja na pewno będzie łatwiej rozszerzalna. Będzie Ci też również łatwiej pisać do niej testy jednostkowe.

Jeżeli taki artykuł Ci się spodobał, to koniecznie dołącz do mojej społeczności. Zapisz się na darmowy newsletter, gdzie co tydzień dzielę się wartościowymi materiałami w szczególności dotyczącymi C# i platformy .NET (darmowy zapis – newsletter).

Poprzedni artykuł - Pierwsza Aplikacja Desktopowa WPF w C# – UI w XAML (1/2).
Następny artykuł - Pierwsza Aplikacja Webowa ASP.NET Core w C# – UI w Razor (1/2).
Autor artykułu:
Kazimierz Szpin
Kazimierz Szpin
CTO & Founder - FindSolution.pl
Programista C#/.NET. Specjalizuje się w Blazor, ASP.NET Core, ASP.NET MVC, ASP.NET Web API, WPF oraz Windows Forms.
Autor bloga ModestProgrammer.pl
Komentarze (4)
Pszyjaciel
PSZYJACIEL, sobota, 2 października 2021 00:18
Kazik, plis wez zapomnij o tych anglicyzmach i urzywaj polskiego jezyka, bo tekst czasami zblirza sie do googlowego tlumaczenie z angielskiego. np.: Zdecydowanie było to chyba prostsze rozwiązanie do zaimplementowania. = Zdecydowanie chyba latwiej bylo napisac kod do tego rozwiazania. Poza jezykiem to fajne merytoryczne podejscie. Dzieki.
Pszyjaciel
PSZYJACIEL, sobota, 2 października 2021 00:20
A i jeszcze jedno: gdzie dajesz linka z paczka do projektu do pobrania, bo nie moge znalezc.
Kazimierz Szpin
KAZIMIERZ SZPIN, niedziela, 3 października 2021 16:35
@PSZYJACIEL, Poprawie się :) Co do paczki do projektu, to muszę dopiero założyć jakieś publiczne repozytorium i będę udostępniał kod.
Jacek
JACEK, środa, 31 sierpnia 2022 13:17
Dzięki za proste wyjaśnienie działania wzorca MVVM w WPF. Właśnie przechodzę przez ten etap i bardzo mi pomogłeś. Docelowo już powoli zaczynam poznawać .NET Maui i to staje się przyszłością. Narzędzie M$ tools MVVM zaczyna także upraszczać życie. Proszę rozważyć przepisanie tego przykładu z wykorzystaniem .NET Maui i MVVM a potem może z narzędziem M$ tools. Generalnie dziękuję i czekam na rozwinięcie tematu.
Dodaj komentarz

Wyszukiwarka

© Copyright 2024 modestprogrammer.pl. Wszelkie prawa zastrzeżone. Regulamin. Polityka prywatności. Design by Kazimierz Szpin