C#/WPF/MVVM - komunikacja między ViewModels

1

Witam,

jestem na początku swojej przygody z C# i zatrzymałem się na pewnym zagadnieniu.

W katalogu "Views" mam dwa okna (MainWindowView.xaml oraz RenameProfileView.xaml).
W katalogu "ViewModels" mam dwie klasy (MainWindowViewModel.cs oraz RenameProfileViewModel.cs).
W katalogu "Models" mam model opisujący profil (plik ProfileModel.cs). Mam tam różne właściwości, np. public string Name { get; set; }

W MainWindowViewModel tworzę kolekcję (ObservableCollection<ProfileModel>) do której dodaję wszystkie znalezione profile (są w osobnych plikach json).
Kolekcję mam zbindowaną do kontrolki ComboBox w MainWindowView. Wszystko działa i w ComboBox mam swoje profile :)

Idąc dalej dodałem nową właściwość, która przechowuje aktualnie wybrany profil - i właśnie z nią będę miał później problem.

        private ProfileModel _selectedProfile;
        public ProfileModel SelectedProfile
        {
            get { return _selectedProfile; }
            set { _selectedProfile = value; }
        }  

Nową właściwość zbindowałem do "SelectedItem" w moim ComboBoxie. Jest ok, działa jak powinno.

Teraz dla wybranego w ComboBoxie profilu chcę zmienić nazwę - w nowym oknie.
Dodałem przycisk, do którego mam podpięte polecenie - jego zadaniem jest otworzenie nowego okna (RenameProfileView).
Zrobiłem implementację RelayCommand, tak więc przycisk działa ok.

Jeżeli to istotne, to kod otwierający okno wygląda tak:

    private void RenameProfile(object parameter)
    {
        _ = new RenameProfileView().ShowDialog();
    }

...i w tym momencie stanąłem przed problemem.
W nowym oknie mam TextBoxa, w którym chciałem wyświetlić nazwę wybranego wcześniej profilu. Czyli powinienem przesłać "SelectedProfile" do tego okna.

Sadzę, że rozwiązałem to w mało elegancki sposób:
Zmieniłem swoją właściwość "selectedProfile" na statyczną, a w RenameProfileViewModel dodałem:

using static MyFirstProgram.ViewModels.MainWindowViewModel;

Teraz oczywiście mam już dostęp do mojego "SelectedProfile". Mogę korzystać ze wszystkich właściwości mojego obiektu, w tym właśnie z "Name".

Zacząłem jednak czytać o komunikacji między ViewModels i natknąłem się na coś o nazwie "mediator".
Nie wiem czy dobrze zrozumiałem ideę tego rozwiązania... czy mogę z tego skorzystać właśnie do przesłania mojego "SelectedProfile" i wtedy nie będzie musiała być statyczna? Jeżeli tak, to czy bawić się w napisanie własnego "mediatora", czy skorzystać z biblioteki typu "MVVM Light" i rozwiązań, które dostarcza - w tym wypadku Messengera?

A może jeszcze inaczej rozwiązać temat z przesłaniem mojego "SelectedProfile"?

Dzięki za pomoc :)

0

Jesli chcesz się dowiedziec jak działa to możesz napisać własnego mediatora, niemniej jednak uważam, że w każdym innym przypadku nie warto, ponieważ to co jest w bibliotece, czy to MVVM Light czy to Prism, jest na pewno lepiej przemyslane.

Prism ma fajnego EventAggregatora, aczkolwiek prawie nic już z tego nie pamiętam.

1

Jeżeli uruchamiasz okno Dialog.Show() możesz w oknie dialog utworzyć DependencyProperty i ustawić w nim co chcesz.

Jeżeli dialog jest w formie kontrolki użytkownika sytuacja jest prosta:
MyUserControl.xaml.cs

    public partial class MyUserControl : Window
    {
        public MyUserControl()
        {
            InitializeComponent();
        }

        public string MyText
        {
            get { return (string)GetValue(MyTextProperty); }
            set { SetValue(MyTextProperty, value); }
        }
        public static readonly DependencyProperty MyTextProperty =
            DependencyProperty.Register("MyText", typeof(string), typeof(MyUserControl), new PropertyMetadata(""));
    }

Bindowanie dwustronne - MyUserControl.xaml

<UserControl x:Class="WPFApp.MyUserControl"
             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:WPFApp"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800"
             x:Name="hUserControl">
    <Grid>
        <TextBox Text="{Binding ElementName=hUserControl, Path=MyText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
    </Grid>
</UserControl>

Teraz można powiązać właściwość z naszego MainWindowViewModel z DependencyProperty w kontrolce:
MainWindowViewModel.cs

    public class MainWindowViewModel : INotifyPropertyChanged
    {
        private string myText;
        public string MyText
        {
            get { return myText; }
            set { myText = value; OnPropertyChanged("MyText"); }
        }


        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged([CallerMemberName] String propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

Oraz binding DependencyProperty z kontrolki MainWindow.xaml

<Window x:Class="WPFApp.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"
        xmlns:local="clr-namespace:WPFApp"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.DataContext>
            <local:MainWindowViewModel x:Name="ViewModel"/>
        </Grid.DataContext>
        <local:MyUserControl MyText="{Binding Path=MyText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
    </Grid>
</Window>

Sytuacja jest trochę inna w przypadku dialogów modalnych. Bez sensu używać wtedy bindingu. Wystarczy przekazać obiekt w konstruktorze:
MyDialog.xaml.cs

    public partial class MyDialog : Window
    {
        public MyDialog(string MyText)
        {
            InitializeComponent();
            this.MyText = MyText;
        }

        public string MyText
        {
            get { return (string)GetValue(MyTextProperty); }
            set { SetValue(MyTextProperty, value); }
        }
        public static readonly DependencyProperty MyTextProperty =
            DependencyProperty.Register("MyText", typeof(string), typeof(MyDialog), new PropertyMetadata(""));
    }

MyDialog.xaml

<Window x:Class="WPFApp.MyDialog"
        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:WPFApp"
        mc:Ignorable="d"
        Title="MyDialog" Height="450" Width="800"
        x:Name="hDialog">
    <Grid>
        <TextBox Text="{Binding ElementName=hDialog, Path=MyText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
    </Grid>
</Window>

Przykład użycia:
MainWindow.xaml.cs

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            MyDialog dialog = new MyDialog("Mój tekst");
            if(dialog.ShowDialog() == true)
            {
                MessageBox.Show("Tekst po zamknięciu dialogu: " + dialog.MyText);
            }
        }
        
    }
0

Bardzo dziękuję za odpowiedź z przykładami :)
Nie wiem czy dzisiaj dam rade się z tym zapoznać... oczy mi już odmawiają posłuszeństwa.
W każdym razie szybko postaram się dokładnie przeanalizować ten kod :)

Jeszcze delikatnie wrócę do MVVM Lite - jak to w ogóle wygląda w firmach... faktycznie korzysta się z takich bibliotek, czy to raczej odosobnione przypadki?
A jeżeli korzysta, to z którego rozwiązania najczęściej - MVVM Lite, Prism, Calibrum Micro, a może jeszcze z czegoś innego?

1

gswidwa1, mam pytanie...
W Twoim przykładzie kontrolka użytkownika - MyUserControl.xaml.cs - dziedziczy po "Window". Dlaczego nie po "UserControl"?
Z tego co widzę, to przy tworzeniu kontrolek użytkownika standardowo jest to właśnie "UserControl".
Chyba, że nie ma to w ogóle znaczenia :)

0

Ma wielkie znaczenie, po prostu na szybko przerabiałem kod i zapomniałem tam zmienić :) powinno być User Control, ale plusik za czujność 😁

0

Czy rozumienie ViewModel że to taki trochę Prezenter (tylko bez referencji do widoku) używający Poleceń i INotifyPropeetyChanged do sterowania widokiem jest poprawne?

Właśnie podchodzę do przenoszenia swojego projektu z WinForms w MVP na WPF w MVVM, przy okazji wchodząc głębiej w MVVM więc nie wiem czy to dobrze rozumiem.

2

Tak, P pełni taką samą rolę w MVP, jak VM w MVVM, czyli odpowiada za logikę widoku. Jedyna różnica jest w sposobie komunikacji z widokiem, gdzie w WPF mamy znacznie bardziej elastyczny system bindingów.

1 użytkowników online, w tym zalogowanych: 0, gości: 1