Laden...

Wert aus Viewmodel an ein anderes Model/Viewmodel übergeben

Erstellt von palexg vor 2 Jahren Letzter Beitrag vor 2 Jahren 613 Views
P
palexg Themenstarter:in
4 Beiträge seit 2022
vor 2 Jahren
Wert aus Viewmodel an ein anderes Model/Viewmodel übergeben

Hallo zusammen,

erstmal vorneweg, ich bin von meinen Kenntnissen zu MVVM den Anfängern zuzuordnen. Ich habe mich vorher der Suche bedient, allerdings meinen konkretes Problem nicht gefunden und bitte daher freundlich um eure unterstützung.

Ich versuche seit ein paar Tagen folgendes Problem zu lösen:

Meine Anwendung besteht aus einem Window in dem ein Frame enthalten ist. In dieses Frame lade ich anlassbezogen Pages (View) zur Darstellung der Daten aus den jeweiligen Viewmodels.
Nun möchte ich Fehlermeldungen/Abschlussmeldungen die aus den verschiedenen Viewmodels resultieren in einem zentralen Textfeld auf dem MainWindow anzeigen lassen.
Mein Ansatz war es ein separates Message Model/Viewmodel zu erstellen und per Binding an des Textfeld im MainWindow zu knüpfen. Der Messagetext wird aus dem Viewmodel der aktiven View (hier am Beispiel des CustomerViewModels) an das Message Model übergeben.

Leider passiert nichts, auch das übergeben des Messagetext an das MessageViewmodel (NewMessage) hat nicht funktioniert. Was mache ich hier falsch? Ist das vielleicht der gänzlich falsche Ansatz?



VIEW#
<Window x:Class="WaWiP.MainWindow"
xmlns:localVM="clr-namespace:WaWiP.ViewModels"
…

<StackPanel HorizontalAlignment="Center">
     <StackPanel.DataContext>
       <localVM:MessageViewModel/>
     </StackPanel.DataContext>
     <Label>Meldung:</Label>
     <TextBlock x:Name="txtMessage" Text="{Binding NewMessage, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>



#MESSAGEVIEMODEL

using System.ComponentModel;
using System.Runtime.CompilerServices;
using WaWiP.Models;

namespace WaWiP.ViewModels
{
    class MessageViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

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

        public Message NewMessage { get; set; }

        public MessageViewModel()
        {
            NewMessage = new Message();
        }
    }
}

#MESSAGE
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;

namespace WaWiP.Models
{
    public class Message : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

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

        private string _Text;

        public string Text
        {
            get { return _Text; }
            set { _Text = value; Changed(); }
        }

    }
}



VIEWMODEL CUSTOMER

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using WaWiP.Commands;
using WaWiP.HelperClass;
using WaWiP.Models;

namespace WaWiP.ViewModels
{
    class CustomerViewModel : INotifyPropertyChanged
    {
        #region INotifyPropertyChanged_Implementation

        public event PropertyChangedEventHandler PropertyChanged;

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

        Message objMessage;
        CustomerService objCustomerService;

        public CustomerViewModel()
        {
            objCustomerService = new CustomerService();
            //LoadData();
            CurrentCustomer = new Customer();
            updateCommand = new RelayCommand(Update);
            objMessage = new Message();
        }

       
        #region DisplayOperations
        private ObservableCollection<Customer> customerList;

        public ObservableCollection<Customer> CustomerList
        {
            get { return customerList; }
            set { customerList = value; Changed(); }
        }

        private void LoadData()
        {
            //Bei initialisierung werden nachfolgenden Daten bereitgestellt
            CustomerList = new ObservableCollection<Customer>(objCustomerService.GetAll());
        }
        #endregion

        private Customer currentCustomer;

        public Customer CurrentCustomer
        {
            get { return currentCustomer; }
            set { currentCustomer = value; Changed(); }
        }

        #region UpdateOperations

        private RelayCommand updateCommand;

        public RelayCommand UpdateCommand
        {
            get { return updateCommand; }
        }

        public void Update()
        {
            try
            {
                var IsUpdated = objCustomerService.Update(CurrentCustomer);
                if (IsUpdated)
                {

                    objMessage.Text = "Die Kundendaten wurden aktualisiert";
                    //LoadData();
                }
                else
                {
                    objMessage.Text = "Update Operation failed";
                }
            }
            catch (Exception Ex)
            {
                objMessage.Text = Ex.Message;
            }
        }
        #endregion

    }
}

T
2.224 Beiträge seit 2008
vor 2 Jahren

Bin jetzt kein WPF Spezi aber du implementierst das INotifyPropertyChanged nicht korrekt!
Deine ViewModel müssen dies beim setter der Properties mit dem Namen (nameof(Property)) aufrufen.
Sonst kann auch eine Änderung an den Models nicht ermittelt werden!

Ebenfalls fehlt deiner Klasse MessageViewModel ein public vor dem class.
Dadurch ist diese per default als privat markiert!
Ist auch bei deinem CustomerViewModel der Fall.

Nachtrag:
Hier noch der Artikel für die Implementierung:
[Artikel] INotifyPropertyChanged implementieren

Nachtrag 2:
Beim Binding hast du als Mode OneWay.
Müsste hier nicht TwoWay stehen?
Bin jetzt nicht sicher ob OneWay von Code oder Xaml Seite betrachtet wird.

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

G
16 Beiträge seit 2019
vor 2 Jahren

T-Virus da muss ich leider widersprechen. Durch CallerMemberName ist der Name optional und wird somit von der übergebenen Property genommen, passt also.

Durch Nichtangabe von public wird es doch internal und nicht private, sollte somit auch passen.

Du bindest auf die Klasse NewMessage und nicht auf die Property Text von Message.
Ich denke mal dass da der Fehler ist?

T
2.224 Beiträge seit 2008
vor 2 Jahren

@Gasimodo
Ah okay, steht auch im Artikel ganz unten.
Bin halt nicht ganz fit in WPF.

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

P
palexg Themenstarter:in
4 Beiträge seit 2022
vor 2 Jahren

T-Virus da muss ich leider widersprechen. Durch CallerMemberName ist der Name optional und wird somit von der übergebenen Property genommen, passt also.

Durch Nichtangabe von public wird es doch internal und nicht private, sollte somit auch passen.

Du bindest auf die Klasse NewMessage und nicht auf die Property Text von Message.
Ich denke mal dass da der Fehler ist?

Mmh, dann würde ich ja die View mit dem Model, statt dem ViewModel verbinden. Das hatte ich aber auch schon anders probiert und im CustomerViewModel eine instanz des Messageviewmodels erstellt und den Messagetext an NewMessage übergeben, was aber auch nicht funktionierte.

palexg

4.939 Beiträge seit 2008
vor 2 Jahren

Ja, dein Ansatz ist schon falsch.

In CustomerViewModel benötigst du ein (-e Referenz auf) MessageViewModel als Member (also entweder hier per new erzeugen oder aber im Konstruktor oder Setter von außen anlegen).
Und dann mußt du der View dieses MessageViewModel-Objekt als DataContext übergeben. Bisher legst du jedoch in deiner View (XAML) per <localVM:MessageViewModel/> ein eigenes (lokales, d.h. anonymes) ViewModel an, auf das du vom (ViewModel-)Code aus nicht zugreifen kannst.

Edit: Die Stichwörter "Service Locator Pattern" sowie "ViewModelLocator" sollten dir dabei (auch) weiterhelfen, z.B. Binden von ViewModels via Locator oder aber die Verwendung eines MVVM-Frameworks (Prism, MVVMLight, ...).

PS: Bei mehreren ViewModels solltest du eine Basisklasse für diese anlegen, in der du dann die INotifyPropertyChanged-Implementierung hast (und nicht jedesmal den Code dafür duplizieren mußt).

P
palexg Themenstarter:in
4 Beiträge seit 2022
vor 2 Jahren

Damit habe ich erstmal was zum lesen. Mal schauen ob ich das lösen kann.
Super. Danke Dir.

187 Beiträge seit 2009
vor 2 Jahren

Servus,

ganz kann ich dem Code im Eingangspost nicht folgen.
Deshalb eine Eigenkreation, die vielleicht das gewünschte berwerkstelligt.
Wie von Th69 schon angemerkt, solltest Du eine Basisklasse für Deine ViewModels haben:


public class ViewModelBase : INotifyPropertyChanged
    {
        #region INotifyPropertyChanged_Implementation
        public event PropertyChangedEventHandler PropertyChanged;

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

Von dieser Basisklasse leiten die folgenden zwei Klassen ab:


    public class CustomerViewModel : ViewModelBase, IViewModelWithMessage
    {
        public string Message { get; private set; }

        public CustomerViewModel(int i)
        {
            Message = $"Customer {i} is greeting you!";
        }
    }



public class NonCustomerViewModel : ViewModelBase
    {
        public NonCustomerViewModel()
        {
            
        }
    }

Die CustomerViewModel Klasse implementiert auch das Interface für das Message-Property


public interface IViewModelWithMessage
    {
        string Message { get; }
    }

Erzeugt werden die konkreten Klassen im MainWindowViewModel


public class MainWindowViewModel : ViewModelBase
    {
        public int Index { get; set; }

        public List<ViewModelBase> ViewModels { get; set; }

        private string newMessage;

        public string NewMessage
        {
            get { return newMessage; }
            set
            {
                if (newMessage != value)
                {
                    newMessage = value;
                    OnPropertyChanged();
                }
            }
        }

        public ICommand MoveToNextViewModelCmd { get; }

        public MainWindowViewModel()
        {
            Index = 0;
            ViewModels = new List<ViewModelBase>();
            for (int i = 0; i < 2; i++)
            {
                ViewModels.Add(new CustomerViewModel(i));
            }
            for (int i = 0; i < 3; i++)
            {
                ViewModels.Add(new NonCustomerViewModel());
            }
            for (int i = 5; i < 10; i++)
            {
                ViewModels.Add(new CustomerViewModel(i));
            }
            MoveToNextViewModelCmd = new MoveToNextViewModelCommand(this);
        }
    }

Das zugehörige MainWindow


<Window x:Class="Forum.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:Forum"
        mc:Ignorable="d"
        Title="MainWindow">

    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>
    
    <StackPanel HorizontalAlignment="Center">
        <Button Content="Next" Command="{Binding MoveToNextViewModelCmd}" />
        <Label>Meldung:</Label>
        <TextBlock x:Name="txtMessage" Text="{Binding NewMessage}"/>
    </StackPanel>
</Window>

Beim Drücken auf den Button in der Mainview wird in der Kommando-Klasse der Text aus der Customer-Klasse in das MainWindowViewModel übertragen.


internal class MoveToNextViewModelCommand : ICommand
    {
        public event EventHandler? CanExecuteChanged;

        private ViewModelBase viewModel;

        public MoveToNextViewModelCommand(ViewModelBase anyViewModel)
        {
            viewModel = anyViewModel;
        }

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

        public void Execute(object? parameter)
        {
            if (viewModel is MainWindowViewModel mainViewModel)
            {
                if (mainViewModel.Index == mainViewModel.ViewModels.Count)
                {
                    mainViewModel.Index = 0;
                }
                if (mainViewModel.ViewModels[mainViewModel.Index] is IViewModelWithMessage viewModelWithMessage)
                {
                    mainViewModel.NewMessage = viewModelWithMessage.Message;
                }
                else
                {
                    mainViewModel.NewMessage = $"ViewModel at Index { mainViewModel.Index } is of type { mainViewModel.ViewModels[mainViewModel.Index] } and has therefore no Message-Property";
                }
                mainViewModel.Index++;
            }
        }
    }

Ich habe den Quellcode auch auf Github

P
palexg Themenstarter:in
4 Beiträge seit 2022
vor 2 Jahren

Guten Morgen
erstmal herzlichen Dank dafür das ihr euch meinem Problem in der Tiefe angenommen habt (ViewModelBase war schonmal ein toller Hinweis um meinen Code zu verschlanken).

Aber irgendwie komme ich nicht weiter.

Daher versuche ich nochmal meine Absicht besser zu erklären. Ich hoffe es gelingt mir.

Der Zweck des CustomerViewModels ist es (so wie ich verstanden habe trifft das für alle Viewmodels zu), Bindeglied zischen dem Model Customer (Properties Customer) /CustomerService (lädt Daten aus der MySQLDatenbank) und der View Kundenstamm zu sein (das ist eine Page im Frame des MainWindows).

Nun gibt es in meinen Programm mehrere Pages, welche Daten über verschiedene ViewModels beziehen. Die ViewModels haben gemeinsam, dass Sie veranlassen, dass Daten aus der MySQL-Datenbank geladen, gespeichert, aktualisiert oder gelöscht werden. Das funktioniert auch einwandfrei.

Meine Idee war es nun, die evtl. aufkommende Fehlermeldungen nicht über ein gewöhnliches PopUp-Fenster, sondern in einem Textfeld im MainWindow anzeigen zu lassen.

Mein Gedanke war es ein MessageViewModel (was auch als MainWindowViewModel verstanden werden kann) zu implementieren und die Property NewMessage an das Textfeld im MainWindow zu binden. Instanziert würde die Klasse in dem jeweiligen ViewModel, in dem einen Fehlermeldung abgefangen werden soll, also z.B. in der CustomerViewKlasse. Ergebnis und Siegerehrung: Es kommen keine Daten m Textfeld an.

Ich glaube ich habe verstanden, dass die Instanz in der View und der Instanz im ViewModel CustomerViewModel sich nicht kennen und daher keine Daten austauschen. Aber wie verbinde ich beide Instanzen? Das ist mir noch nicht klar. Kann ich irgendwo die gerade aktive Instanz herausfinden und adressieren oder ist mein Ansatz schon falsch und es gibt eine banale Lösung um das gewünschte Ergebnis zu erhalten?

Dieses offensichtlich nicht banale unterfangen, bringt mich bis heute zum verzweifeln.

Ich sage schonmal vielen Dank.

4.939 Beiträge seit 2008
vor 2 Jahren

Wie schon geschrieben, helfen dabei die Frameworks mit entsprechender Umsetzung des Service Locator Patterns.
Von Hand mußt du die Verbindung beider Views und ViewModels selber erstellen, z.B. mittels Data Binding der DataContext-Eigenschaft, s. z.B. "Hierarchical ViewModels" in Binding your View to your ViewModel in Wpf.

Wenn jedoch die Textnachricht im selben View angezeigt werden soll, dann lohnt sich der Aufwand eines eigenen ViewModels (als Anfänger) dafür nicht. Lege daher einfach die Eigenschaft im MainViewModel dafür an (so wie Caveman im vorherigen Beitrag gezeigt).
Und die CustomerViewModels senden dann per Ereignis (event) die Nachricht an das übergeordnete MainViewModel (s.a. [FAQ] Eigenen Event definieren / Information zu Events (Ereignis/Ereignisse)).

187 Beiträge seit 2009
vor 2 Jahren

Ich habe auf Github eine zweite Version veröffentlicht.

In dieser Version wird ein "Navigator" zum Umschalten der ViewModels verwendet.

MainView:


<Window x:Class="Forum124378II.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:Forum124378II"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="300">

    <Window.Resources>
        <DataTemplate DataType="{x:Type local:CustomerViewModel}">
            <local:Customers />
        </DataTemplate>
        <DataTemplate DataType="{x:Type local:DealerViewModel}">
            <local:Dealers />
        </DataTemplate>
    </Window.Resources>
    
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="60" />
        </Grid.RowDefinitions>

        <StackPanel Grid.Row="0"
                    Orientation="Horizontal"
                    HorizontalAlignment="Center">
            <TextBlock Text="Views: "
                       VerticalAlignment="Center"/>
            <ComboBox ItemsSource="{Binding ViewTypes}"
                      SelectedItem="{Binding SelectedViewType}"/>

        </StackPanel>

        <ContentControl Grid.Row="1"
                        HorizontalAlignment="Stretch"
                        Content="{Binding CurrentViewModel}" />

        <TextBlock Grid.Row="2"
                   FontWeight="Bold"
                   Foreground="Red">
            <Run Text="Error: " />
            <Run Text="{Binding ErrorMessage}" />
        </TextBlock>
                   
    </Grid>
</Window>

MainViewModel


using System;
using System.Collections.Generic;
using System.Linq;

namespace Forum124378II
{
    public class MainWindowViewModel : ViewModelBase
    {
        private Navigator navigator;
        private ViewModelFactory viewModelFactory;
        private ViewModelTypes selectedViewType;
        private string errorMessage;

        public ViewModelBase CurrentViewModel
        {
            get { return navigator.CurrentViewModel; }
        }

        public string ErrorMessage
        {
            get { return errorMessage; }
            set
            {
                if (errorMessage != value)
                {
                    errorMessage = value;
                    OnPropertyChanged();
                }
            }
        }

        public IEnumerable<ViewModelTypes> ViewTypes
        {
            get
            {
                return Enum.GetValues(typeof(ViewModelTypes)).Cast<ViewModelTypes>();
            }
        }

        public ViewModelTypes SelectedViewType
        {
            get { return selectedViewType; }
            set
            {
                if (selectedViewType != value)
                {
                    selectedViewType = value;
                    OnPropertyChanged();
                    UpdateView();
                }
            }
        }

        public MainWindowViewModel()
        {
            navigator = new Navigator();
            viewModelFactory = new ViewModelFactory();

            navigator.StateChanged += ViewModelChanged;
            SelectedViewType = ViewModelTypes.Customers;
            navigator.CurrentViewModel = viewModelFactory.Get(SelectedViewType);
        }

        private void UpdateView()
        {
            navigator.CurrentViewModel = viewModelFactory.Get(SelectedViewType);
        }

        private void ViewModelChanged()
        {
            OnPropertyChanged(nameof(CurrentViewModel));
            if (CurrentViewModel is IMessage viewModelWithMessage)
            {
                ErrorMessage = viewModelWithMessage.Message;
            }
        }
    }
}

Navigator:


using System;

namespace Forum124378II
{
    public class Navigator
    {
        private ViewModelBase currentViewModel;

        public ViewModelBase CurrentViewModel
        {
            get
            {
                return currentViewModel;
            }
            set
            {
                currentViewModel = value;
                StateChanged?.Invoke();
            }
        }

        public event Action StateChanged;
    }
}

Wenn der Navigator ein neues ViewModel erhält, dann wird ein Event gefeuert.
Das MainViewModel hat dieses Event aboniert und reagiert darauf mit der Methode ViewModelChanged.
In der Methode wird die Eigenschaft CurrentViewModel aktualisiert, was einen Austausch des DataTemplates in der View zur Folge hat.
Weiterhin wird geprüft, ob das ViewModel die Schnittstelle IMessage implementiert.
Falls dem so ist, dann wird der Wert aus der Eigenschaft Message im CurrentViewModel in die Eigenschaft ErrorMessage geschrieben.