Wprawka do oceny i podpowiedzi

0

Potrzebuje dobrze zrozumieć zasadę tworzenia i stosowania DependencyProperty. Zacząłem dostawać zadanka rekrutacyjne gdzie są aspekty które najprawdopodobniej wymagają zastosowania własności zależnych, po mojej obecnej implementacji kontakt z rekruterem się urywa i wyciągam wniosek że w moim rozwiązaniu zadania "dzieje się mocno źle" ;).

W swojej wprawce wyciągnąłem sobie parę przypadków gdzie chciałbym je zastosować a które do tej pory sprawiają mi sporo problemów w WPF MVVM (w code behind to proste rzeczy)

Generalnie zadania z tej wprawki zrobiłem sobie takie:
obraz_2022-11-26_080834036.png

Pierwszy punkt zrobiłem tak jak niżej i działa, pytanie czy to na tym polega?
Tyle ma być nadmiarowości w stosunku do code behind, czy tak to się robi?

Tworze klasę opakowującą klasyczny Button i dodaje w niej dwie właściwości zależne:
Założyłem ze będę to realizował w ten sposób ze dodam właściwość do przycisku trzymającą referencje do okna rodzica, i druga właściwość Boolean, której zmiana na true wywoła metodę Close() na referencji okna trzymanej w tej drugiej właściwości. Kod poniżej.

public class BtnZad1 : Button
{
    public static readonly DependencyProperty BooleanZamykajacyProperty;
    public static readonly DependencyProperty OknoRodzicBtnZad1Property;

    static BtnZad1()
    {
        BooleanZamykajacyProperty = DependencyProperty
            .Register("ZamknijOkno",
            typeof(bool),
            typeof(BtnZad1),
            new PropertyMetadata(false, OnKlikniecieDoZamkniecia));

        OknoRodzicBtnZad1Property =
        DependencyProperty.Register(
            "OknoRodzicBtnZad1",
            typeof(Window),
            typeof(BtnZad1),
            new PropertyMetadata(null));
    }

    public Window OknoRodzicBtnZad1
    {
        get { return (Window)GetValue(OknoRodzicBtnZad1Property); }
        set { SetValue(OknoRodzicBtnZad1Property, value); }
    }

    public bool ZamknijOkno
    {
        get { return (bool)GetValue(BooleanZamykajacyProperty); }
        set { SetValue(BooleanZamykajacyProperty, value); }
    }

    static void OnKlikniecieDoZamkniecia(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        (d as BtnZad1).OnKlikniecieDoZamkniecia(e);
    }

    void OnKlikniecieDoZamkniecia(DependencyPropertyChangedEventArgs e)
    {
        if (e.NewValue is true && OknoRodzicBtnZad1 != null)
            OknoRodzicBtnZad1.Close();
    }
}

w Swoim ViewModelu okna głównego wystawiam właściwość Boolean którą powiążę ze swoją nowo utworzoną własnością zależną ustawiam ja na początku na true (nie ustawiaj jej, bo dałem i tak wartość domyślną podczas rejestracji false), no i dodaje w ViewModelu obsługę polecenia które podpinam do tego klawisza.

public class ViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string nazwaWlasciwosci)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nazwaWlasciwosci));
    }

    public ViewModel()
    {
    }

    #region 1.
    private bool czyZamknac;
    public bool CzyZamknac
    {
        get { return czyZamknac; }
        set
        {
            czyZamknac = value;
            OnPropertyChanged(nameof(CzyZamknac));
        }
    }
    private ICommand klikniecieZamykajace;
    public ICommand KlikniecieZamykajace
    {
        get
        {
            if (klikniecieZamykajace == null)
                klikniecieZamykajace = new RelayCommand(Zamknij);
            return klikniecieZamykajace;
        }
    }
    private void Zamknij(object o)
    {
        //jakies dzialania konieczne do zrobienia przed zamknieciem
        CzyZamknac = true;
    }
}

a w XAMLu definiuje wiązania w tym przycisku tak:

<local:BtnZad1 Grid.Row="0" Grid.Column="1"  
    OknoRodzicBtnZad1="{Binding ElementName=OknoRodzic}"
    Command="{Binding KlikniecieZamykajace}"
    ZamknijOkno="{Binding CzyZamknac}"
    Width="120" 
    Height="30" 
    HorizontalAlignment="Center" 
    VerticalAlignment="Center">
  <TextBlock Text="Zamknij okno"/>            
</local:BtnZad1>

Rozwiązanie działa czyli warunek spełniony, ale czy tak się robi?
W analogiczny sposób robię pkt.4.
Z pkt.2. czyli przechwytywaniem tego klawisza Esc to mgliście mi chodzi po głowie ze chyba trzeba rozszerzyć klasę okno, tam dodać właściwość zależną trzymającą klawisz Key.Escape, potem powiązać to poprzez Converter z właściwością boolean z ViewModelu i obsługą polecenia zrobić tak samo?

Pk.3 i 5 tak samo jak pkt.2 tyle ze rozszerzam klawy TextBox i PasswordBox?

0

Zad.2. na Starcie srpawia mi problem, bo gdy definiuje takąklase opakowujaca Window, z wlasciwoscia zależna KlawiszZamykający, nie jestem w stanie w XAMLu utworzyc zamiast glownego znacznika window, znacznika WindowZad2.

public class WindowZad2 : Window
    {
        public Key KlawiszZamykajacy
        {
            get { return (Key)GetValue(KlawiszZamykajacyProperty); }
            set { SetValue(KlawiszZamykajacyProperty, value); }
        }

        public static readonly DependencyProperty KlawiszZamykajacyProperty =
            DependencyProperty.Register("KlawiszZamykajacy", typeof(Key), typeof(WindowZad2), new PropertyMetadata(Key.Escape));

    }

w XAMLU nie moge tak zrobić, bo krzyczy ze taki tyop nie wystepuje w przestrzeni nazw 'WPF_DependencyProperty':obraz_2022-11-26_092837457.png

1

Odniosę się tylko do pierwszego, zarówno nowa kontrolka jak i dependency property nie są potrzebne. W rzeczywistości stosuje się je bardzo rzadko, kiedy rzeczywiście mamy do napisania reużywalny fragment kodu. Jeśli nie było wyraźnego polecnia, ze to mają być kontrolki, to powinno być wszystko zrobione w VM, ewentualnie przy pomocy attached propertiesów czy behaviours by powiązać zdarzenia na klawiaturze z VM.

0

pk2. poprzez behavior zrobiłem tak, ze zdefiniowałem taka klasę (rozumiem ze takie zachowania definiuje sie w Widoku?)

    public class ZamkniecieOknaPoprzezKlawisz : Behavior<Window>
    {
        public Key KlawiszZamykajacy { get; set; }
        protected override void OnAttached()
        {
            Window okno = this.AssociatedObject as Window;
            if (okno!= null)
                okno.PreviewKeyDown += ObslugaZamkniecia;
        }

        private void ObslugaZamkniecia(object sender, KeyEventArgs e)
        {
            Window okno = (Window)sender;
            if (e.Key == KlawiszZamykajacy)
                okno.Close();
        }
    }

a w XAMLu okna dodałem to tak:

<Window x:Class="WPF_DependencyProperty.MainWindow"
       
		xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        
        mc:Ignorable="d"
        Title="Wprawki DependencyProperty, wszystko ma być zgodne z MVVM" 
        Height="450" Width="600">
        
    <i:Interaction.Behaviors>
        <local:ZamkniecieOknaPoprzezKlawisz KlawiszZamykajacy="Esc"/>
    </i:Interaction.Behaviors>
0

Trochę się zaplątałem z pkt.3. , chce tu najpierw dokończyć zachowanie 'KlawiszCzyszczacyBoxBehavior' aby tylko czyściło pole 'Text' ale mi to nie działa, nie wiem jak prawidłowo podpiąc to zachowanie w XAMLu.

(Dopiero jak samo czyszczenie zadziała, będę chciał dodać do tego zachowania polecenie które już będę podpinał w xamlu i one będzie odpowiadać za przepisywanie wartości .)

klasa 'KlawiszCzyszczacyBoxBehavior' wygląda tak: a niżej jak próbuje dołączyć go poprzez XAMLa do TextBox'a.

public class KlawiszCzyszczacyBoxBehavior : Behavior<TextBox>
    {
        public static readonly DependencyProperty KlawiszProperty =
            DependencyProperty.Register(
                "KlawiszCzyszczacy",
                typeof(Key),
                typeof(KlawiszCzyszczacyBoxBehavior),
                new PropertyMetadata(Key.None, ZmianaPrzypisanegoKlawisza));

        public Key KlawiszCzyszczacy
        {
            get { return (Key)GetValue(KlawiszProperty); }
            set { SetValue(KlawiszProperty, value); }
        }


        private static void ZmianaPrzypisanegoKlawisza(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            KeyEventHandler obslugaNacisnieciaKlawisza =
                (object sender, KeyEventArgs _e) =>
                {
                    if (_e.Key == (d as KlawiszCzyszczacyBoxBehavior).KlawiszCzyszczacy)
                        ((TextBox)sender).Text = "";
                };

            (d as KlawiszCzyszczacyBoxBehavior).AssociatedObject.KeyDown += obslugaNacisnieciaKlawisza;
        }
    }

obraz_2022-11-26_225905414.png

0

1. Pytanie do rozwiązania dla pkt.3.

Trochę bliżej z pkt.3. ale na razie działa jedynie czyszczenie 'TextBoxa',
polecenie które ma przepisać wartość do drugiego 'TextBoxa' wykonuje się ale, jak podglądam w debugerze co się dzieje w metodzie tego polecenia 'private void ObslugaPrzepisaniaTekstuDoDrgiego(object o)' to tak jakby ta metoda wywołuje się bez uprzedniego zaktualizowania wartości pól ViewModelu z kontrolek TextBox na widoku (wiązanie wygląda ok).

Dzieje się tak:
. uruchamiam program
. w TextBoxie który ma obsługę 'Entera' jest już wartość ustawiona w konstruktorze ViewModelu równa "poczat",
. cokolwiek dopisze w tym TextBoxie to do drugiego zostanie przeniesiona wartość "poczat" i ten pierwszy TextBox zostanie wyczyszczony
. ale za drugim razem to już cokolwiek bym nie napisał w tym pierwszym TextBoxie to po naciśnięciu 'Enter' do drugiego przepisywany jest pusty string a pierwszy się czyści

Co tu się dzieje? i dlaczego tak?

moja definicja w sumie nie wiem czego; zachowania, właściwości doczepianej, zachowania doczepianego? - nie mam pojęcia jak odróżnić jedno od drugiego?

public class KlawiszCzyszczacyBoxBehavior : Behavior<TextBox>
    {
        public Key KlawiszCzyszczacyTextBox { get; set; }
        public static readonly DependencyProperty PoleceniePrzedWyczyszczeniemProperty =
            DependencyProperty.RegisterAttached(
                "PoleceniePrzedWyczyszczeniem",
                typeof(ICommand),
                typeof(KlawiszCzyszczacyBoxBehavior));


        public ICommand PoleceniePrzedWyczyszczeniem
        {
            get { return (ICommand)GetValue(PoleceniePrzedWyczyszczeniemProperty); }
            set { SetValue(PoleceniePrzedWyczyszczeniemProperty, value);}
        }


        protected override void OnAttached()
        {
            TextBox txtBox = this.AssociatedObject as TextBox;

            if (txtBox!= null)
                txtBox.PreviewKeyDown += TxtBox_KeyDown;    // tu probowalem tez z txtBox.KeyDown += , ale nie ma roznicy
        }

        private void TxtBox_KeyDown(object sender, KeyEventArgs e)
        {
            TextBox textBox = (TextBox)sender;
            if (e.Key == KlawiszCzyszczacyTextBox)
            {
                PoleceniePrzedWyczyszczeniem.Execute(null);
                textBox.Text = "";
            }
        }
    }

tak to podlaczam w XAMLu

<TextBox x:Name="txtBox1_pkt3"             
                    Text="{Binding TxtBox_3_1}"
                    Height="20" 
                    Width="160"
                    Margin="2">
                    <i:Interaction.Behaviors>
                        <local:KlawiszCzyszczacyBoxBehavior KlawiszCzyszczacyTextBox="Return" 
                                                            PoleceniePrzedWyczyszczeniem="{Binding Przepisz_ztxtB31_dotxtB32}"/>
                    </i:Interaction.Behaviors>
                </TextBox>

a tak to jest zbindowane w ViewModelu i tak wygląda polecenie w ViewModelu

public class ViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string nazwaWlasciwosci)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nazwaWlasciwosci));
        }

        public ViewModel()
        {
            TxtBox_3_1 = "poczat";
        }
        private string txtBox_3_1;
        private string txtBox_3_2;
        public string TxtBox_3_1
        {
            get { return txtBox_3_1; }
            set
            {
                txtBox_3_1 = value;
                OnPropertyChanged(nameof(TxtBox_3_1));
            }
        }
        public string TxtBox_3_2
        {
            get { return txtBox_3_2; }
            set
            {
                txtBox_3_2 = value;
                OnPropertyChanged(nameof(TxtBox_3_2));
            }
        }

        private ICommand przepisz_ztxtB31_dotxtB32;
        public ICommand Przepisz_ztxtB31_dotxtB32
        {
            get
            {
                if (przepisz_ztxtB31_dotxtB32 == null)
                    przepisz_ztxtB31_dotxtB32 = new RelayCommand(ObslugaPrzepisaniaTekstuDoDrgiego);
                return przepisz_ztxtB31_dotxtB32;
            }
        }
        private void ObslugaPrzepisaniaTekstuDoDrgiego(object o)
        {
            TxtBox_3_2 = TxtBox_3_1;
        }

2. Pytanie do rozwiązania dla pkt.5.

W pkt.5. podpiąłem obsługę zdarzenia 'PasswordChanged' w codebehind opakowanego PasswordBoxa w nowa kontrolkę z wystawiona właściwością 'PasswordBindowalne' i tam z kolei, ładnie przepisuje się wartość z BindowalnyPasswordBoxa do TextBoxa, ale po całej akcji nie jest czyszczona kontrolka PasswordBox.Password (zostają te kropki, chyba winne jest to ze w 'BindowalnyPasswordBox' )

Definicja własnej kontrolki która opakowuje standardowego PasswordBox'a

public partial class BindowalnyPasswordBox : UserControl
    {
        public BindowalnyPasswordBox()
        {
            InitializeComponent();
            opakowywanyPasswordBox.PasswordChanged += OnPasswordChanged;
        }

        public static readonly DependencyProperty PasswordBindowalneProperty =
            DependencyProperty.Register(
                "PasswordBindowalne",
                typeof(string),
                typeof(BindowalnyPasswordBox));

        public  string PasswordBindowalne
        {
            get { return (string)GetValue(PasswordBindowalneProperty); }
            set { SetValue(PasswordBindowalneProperty, value); }
        }

        private  void OnPasswordChanged(object o, RoutedEventArgs e)
        {
            PasswordBindowalne = (o as PasswordBox).Password;
        }
    }

jego XAML

<UserControl x:Class="WPF_DependencyProperty.BindowalnyPasswordBox"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:WPF_DependencyProperty"
             mc:Ignorable="d" 
             d:DesignHeight="40" d:DesignWidth="280">
    <PasswordBox x:Name="opakowywanyPasswordBox"/>
</UserControl>

podpięcie w XAMLu wygląda tak:

<local:BindowalnyPasswordBox
                    PasswordBindowalne="{Binding PwdBox_5, Mode=TwoWay}"
                    Width="160"
                    Height="20">
                    <local:BindowalnyPasswordBox.InputBindings>
                        <KeyBinding Key="Return"
                                    Command="{Binding PrzepiszHasloIWyczyscBox}"/>
                    </local:BindowalnyPasswordBox.InputBindings>

                </local:BindowalnyPasswordBox>

a w ViewModelu bindowanie i polecenie wygląda tak:

        private string pwdBox_5;
        private string txtBox_5;
        public string PwdBox_5
        {
            get { return pwdBox_5; }
            set
            {
                pwdBox_5 = value;
                OnPropertyChanged(nameof(PwdBox_5));
            }
        }
        public string TxtBox_5
        {
            get { return txtBox_5; }
            set
            {
                txtBox_5 = value;
                OnPropertyChanged(nameof(TxtBox_5));
            }
        }

        private ICommand przepiszHasloIWyczyscBox;
        public ICommand PrzepiszHasloIWyczyscBox
        {
            get
            {
                if (przepiszHasloIWyczyscBox == null)
                    przepiszHasloIWyczyscBox = new RelayCommand(ObslugaTegoZdarzenia);
                return przepiszHasloIWyczyscBox;
            }
        }
        private void ObslugaTegoZdarzenia(object o)
        {
            TxtBox_5 = PwdBox_5;
            PwdBox_5 = "";
        }

Całość wypocin tutaj:
Cały projekt na GitHubie

2

Myliłem się, w dzisiejszych czasach to nawet można bindindować do klawiszy prosto z xamla, bez żadnych dodatkowych cudów. Więc jak zamknąć okno po naciśnięciu klawisza ESC, zgodnie z MVVM i SOLID i własnym sumieniem:

<Window x:Class="WpfMVVMKeyDown.MainWindow"
        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"             
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.InputBindings>
        <KeyBinding Key="Esc" Command="{Binding CloseCmd}" />
    </Window.InputBindings>
    <Grid>

    </Grid>
</Window>
using System.Windows;

namespace WpfMVVMKeyDown
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window, ICanBeClosed
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}
namespace WpfMVVMKeyDown
{
    internal interface ICanBeClosed
    {
        void Close();
    }
}
using WpfMVVMKeyDown.WPF;

namespace WpfMVVMKeyDown
{
    internal class MainWindowViewModel : BaseViewModel
    {
        private readonly ICanBeClosed closeMe;  

        public RelayCommand CloseCmd { get; }


        public MainWindowViewModel(ICanBeClosed closeMe)
        {
            CloseCmd = new RelayCommand(x => closeMe.Close());
        }
    }
}
using System.Windows;

namespace WpfMVVMKeyDown
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);

            var mainWindow = new MainWindow();
            mainWindow.DataContext = new MainWindowViewModel(mainWindow);
            mainWindow.ShowDialog();
        }
    }
}

Analogicznie wszytkie te zadania można zrobić wykorzystując InputBindings, żadne DP czy AP i dobieranie się do kontrolek nie jest potrzebne.

3

Dużym problemem dla rekruterów może też być programowanie "po polsku". Rób to tylko po angielsku. Wszystkie kontrolki, zmienne, metody itd. Wszystko nazywaj po angielsku.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.