Laden...

MVVM - Woher bekommt ein ViewModel übergreifende Daten? Von parallelen ViewModellen oder von darunterliegenden Modellen?

Erstellt von StayCalm vor 5 Monaten Letzter Beitrag vor 5 Monaten 526 Views
S
StayCalm Themenstarter:in
8 Beiträge seit 2023
vor 5 Monaten
MVVM - Woher bekommt ein ViewModel übergreifende Daten? Von parallelen ViewModellen oder von darunterliegenden Modellen?

Hallo Forumgemeinde,

ich habe erneut eine Anfängerfrage und ich interessiere mich dafür, wie man meine Situation am besten implementiert. Ich habe für mich mein Problem in ein Beispielprojekt heruntergebrochen. Ich könnte zwar dieses minimalistische Projekt hier posten, aber darauf soll laut Forenregel lieber verzichtet werden. Ich bin aber der Meinung, dass ich es auch so erklären kann, ohne dass man mein Projekt überhaupt anschauen muss.

Mein Projekt sieht so aus:

[Model]
-	Animal.cs
-	Combine.cs
-	Person.cs
[View]
-	MainWindow.xaml
[ViewModel]
-	MainViewModel.cs
-	ViewModelAnimal.cs
-	ViewModelCombine.cs
-	ViewModelPerson.cs

Ich habe im MainWindow drei GroupBoxes angelegt:

Die erste GroupBox "Person" hat eine TextBox, in der man einen Namen eintragen kann.

Die zweite GroupBox "Animal" hat eine TextBox, in der man eine Tier-Spezies eintragen kann.

Die dritter GroupBox "Combine" hat einen Button und ein Label. Wenn man in der dritten GroupBox den Button „Combine“ klickt, soll im Label der String aus der Person-TextBox mit dem String des Animal-TextBox zusammengeführt werden.

Was mir nicht klar ist, Woher bekommt das „ViewModelCombine“ die Daten, um den kombinierten String erstellen zu können? Muss in „ViewModelCombine“ zusätzlich die beiden Modelle „Person“ und „Animal“ eingebunden werden, oder werden die beiden ViewModelle „ViewModelPerson“ und „ViewModelAnimal“ eingebunden. Hierarchisch gesehen liegen die ViewModelle nebeneinander. Darf es hier „Verbindungen“ untereinander geben?

Wie schon gesagt, ich habe es auf ein Beispielprojekt herunter gebrochen. Ich habe ein eigenes ViewModel für „Combine“ angelegt, allerdings nur, um es exemplarisch für mich darzustellen. Kann man solche Dinge, die Informationen aus mehreren ViewModels benötigt, in einer Art „globales ViewModel“, oder sogar im MainViewModel abhandeln? Das MainViewModel existiert bei mir auch nur, um darin die beiden anderen zwei ViewModelle einzubinden, so dass diese dann im MainWindow genutzt werden können. In meinem Beispielprojekt finde ich ein eigenes ViewModel für den „Combine“-Button eigentlich übertrieben.

Ich denke eine solche Situation wie ich sie habe, ist bestimmt keine Seltenheit in Projekten und daher interessiert es mich, wie ihr es Implementieren würdet.

Nur noch zur Info: Diese Frage hier basiert auf eine ältere Frage, die ich bereits hier https://mycsharp.de/forum/threads/125590/wpf-mvvm-einbindung-mehrere-viewmodelle-in-einer-view gestellt habe. Die erste Frage, habe ich soweit sehr gut beantwortet bekommen und war Basis für diese zweite Frage.

Viele Grüße 😃

T
2.225 Beiträge seit 2008
vor 5 Monaten

Wenn du nur eine Seite hast, warum hast du dann nicht einfach ein Modell mit entsprechenden Properties mit allen Daten?
Dieses Modell kann dann einfach zwei Properties haben, jeweils für Person und Animal.
Dann kannst du Combine einfach als getter umsetzen, der dann die Anzeige liefert.
Oder denke ich gerade zu einfach?

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.

16.857 Beiträge seit 2008
vor 5 Monaten

Wenn es nur eine Seite ist, dann hast Du ja einfach Dein ViewModel und die gebundenen Daten.

Geht es dir darum, dass Du in mehreren Views Daten hältst und die ViewModels unter sich Informationen austauschen, so macht man das generell über State Management - nicht nur in .NET/WPF sondern quasi in allen UI Frameworks, also auch in Angular, React, Flutter etc...

In .NET zB Reactive Extensions oder ReactiveUI lösen.

S
StayCalm Themenstarter:in
8 Beiträge seit 2023
vor 5 Monaten

Hi T-Virus,

danke für Deine Antwort. Ja, generell hast Du natürlich vollkommen Recht. Gerade was mein minimalistisches Beispiel betrifft, wäre es absolut einfacher, wenn man alles zusammen in einem Model und auch einem ViewModel hat.

Allerdings bin ich an einem anderen Projekt dran, bei dem ich deutlich mehr auf dem MainWindow anzeige. Da kommen schon einige Sachen zusammen und daher habe ich mich dazu entschlossen, nicht alles zusammen zu packen, da ich es übersichtlicher finde. Generell lässt sich das auch gut implementieren da die Teile eigentlich nichts miteinander zu tun haben. Es gibt nur einen Teil, der von einigen anderen Teilen doch Informationen benötigt, damit ich diese gesammelt irgendwie anzeigen kann. Deshalb habe ich jetzt diese Situation.

Ich habe daher die Frage, wenn man in MVVM mehrere ViewModelle hat, wie können diese Informationen austauschen?

129 Beiträge seit 2023
vor 5 Monaten

ViewModel können Daten auch über einen Messaging Dienst austauschen (z.B. ReactiveUI.MessageBus) allerdings würde ich in deinem Beispiel eher dahin tendieren dem MainWindowViewModel alle benötigten ViewModel per DI zu übergeben und als Eigenschaft zur Verfügung zu stellen.

Hat die Blume einen Knick, war der Schmetterling zu dick.

4.955 Beiträge seit 2008
vor 5 Monaten

Oder aber du übergibst die Referenzen auf die beiden anderen VMs an die dritte VM:

CombineViewModel Combine { get; init; }

// im MainViewModel-Konstruktor
Combine = new CombineViewModel(Person, Tier); // Namen entsprechend dem Code aus deinem anderen Thema 

Und im CombineViewModel dann den Konstruktor um diese Parameter erweitern und Eigenschaften dafür anlegen, so daß du dann in den zu bindenden Eigenschaften darauf zugreifen kannst.

PS: Dies wäre dann auch der Ansatz für DI (üblicherweise aber als Interfaces, anstatt konkreter Klassennamen).

A
764 Beiträge seit 2007
vor 5 Monaten

Hallo StayCalm

Zitat von StayCalm

Die dritter GroupBox "Combine" hat einen Button und ein Label. Wenn man in der dritten GroupBox den Button „Combine“ klickt, soll im Label der String aus der Person-TextBox mit dem String des Animal-TextBox zusammengeführt werden.

Wenn es sich bei dem Zusammenführen von Text um komplexe Geschäftslogik handelt, dann sollte sich diese in der entsprechenden Schicht befinden:
https://mycsharp.de/forum/threads/111860/artikel-drei-schichten-architektur

Dein CombineViewModel bekommt dann ein ICommand-Property, dass vom MainViewModel aus gesetzt werden kann, und vom Button ausgelöst wird.

// So ähnlich könnte das aussehen. Habe es ohne Testen runtergeschrieben.
public MainViewModel(CombinationService combinationService)
{
  this.combineViewModel = new CombineViewModel
  {
    CombineCommand = new RelayCommand<string>(() => this.CombinationResult = combinationService.Combine(this.personViewModel.Name, this.animalViewModel.type));
  };
}

Gruss
Alf

S
StayCalm Themenstarter:in
8 Beiträge seit 2023
vor 5 Monaten

Hallo, alles zusammen.

Erst mal vielen Dank für die Vorschläge. Ich musste mich erst mal in die einzelnen Themen grob einlesen.

@BlonderHans:

Das mit dem „MessageBus“ kannte ich noch gar nicht. Was ich auf der Seite von Microsoft lesen kann, ist es schon dafür gedacht, dass ein Publish Event an mehrere Subscriber verteilt werden kann. Nutzt Du solch einen MassageBus in Deinen Projekten? Sind das dann große/Aufwendige Projekte?

DI? Das musste ich auch erst Googlen 😊 „Dependency Injection“. Ich denke in meinem Fall ist gemeint, dass ich dem „ViewModelCombine“ bereits bei der Instanziierung (über den Konstruktor) die beiden Strings mitgeben muss?

@Alf:

Du hast ja einen Link bezüglich eines Schichtmodels gepostet. Aber ist das nicht bereits das, was MVVM macht? Auf mich macht es den Eindruck, als wäre das so. Ich kann mich da aber auch irren.

So wie Du es mit dem ICommand beschrieben hast, nutze ich es bereits. (Ich denke so hast Du es gemeint)

Meine Klassen sehen so aus: (auf die Model-Klassen habe ich verzichten)

ViewModelAnimal.cs:

namespace Fictiv.ViewModel
{
  public class ViewModelAnimal : ObservableObject
  {
    // the viewmodel have to know the model.
    private readonly Model.Animal Animal;
    
    private string animalSpeciesText;
    public string Bind_animalSpeciesText
    { 
    	get { return animalSpeciesText; }
        set { SetProperty(ref animalSpeciesText, value); }
    }
    // Constructor
    public ViewModelAnimal() { }
  }
}

ViewModelCombine.cs:

namespace Fictiv.ViewModel
{
  public class ViewModelCombine : ObservableObject
  {
    // the viewmodel have to know the model.
    private readonly Model.Combine Combine;

    private string combineCombinedText;
    public string Bind_combineCombinedText
    {
          get { return combineCombinedText; }
          set { SetProperty(ref combineCombinedText, value); }
    }

    // button to combine person with animal text
    public ICommand Bind_btnCombineCommand { get; }

    // Constructor
    public ViewModelCombine()
    {
    	Bind_combineCombinedText = "EMPTY";
        // handle binding methods
        Bind_btnCombineCommand = new RelayCommand(BtnCombineCommand);
    }

    private void BtnCombineCommand()
    {
          Bind_combineCombinedText = "Button has been pressed";
        /*
        here the two strings "Person.Bind_personNameText"
        and "Animal.Bind_animalSpeciesText" should be
        added to eachother.
        */
    }
  }
}

ViewModelPerson.cs:

namespace Fictiv.ViewModel
{
  public class ViewModelPerson : ObservableObject
  {
    // the viewmodel have to know the model.
    private readonly Model.Person Person;

    private string personNameText;
    public string Bind_personNameText
    {
    	get { return personNameText; }
        set { SetProperty(ref personNameText, value); }
    }
    
    // Constructor
    public ViewModelPerson() { }
  }
}

MainViewModel.cs:

namespace Fictiv.ViewModel
{
  public class MainViewModel
  {
    public ViewModelAnimal Animal { get; private set; } = new ViewModelAnimal();
    public ViewModelPerson Person { get; private set; } = new ViewModelPerson();
    public ViewModelCombine Combine { get; private set; } = new ViewModelCombine();
  }
}

MainWindow.xaml

…
<Window.DataContext >
  <localVM:MainViewModel/>
</Window.DataContext>
<Canvas Name="FictivExample" IsEnabled="True" >
  <Label x:Name="lblHedline" Width="160" Height="40" HorizontalAlignment="Left" VerticalAlignment="Center"
           FontSize="20" FontWeight="Bold" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
           Canvas.Left="80" Canvas.Top="5"  Content="Fictiv-Example"  />
  <GroupBox x:Name="grBoxPerson" BorderThickness="2" BorderBrush="Green" Width="160" Height="100" Header="Person" VerticalAlignment="Center" 	Canvas.Left="10" Canvas.Top="40" HorizontalAlignment="Left">
    <Grid>
      <StackPanel Orientation="Horizontal" Height="70" Width="135" Margin="0,0,0,0">
        <UniformGrid Columns="2" Width="135" Margin="5,0,0,0" >
        <!--First row-->
        <Label x:Name="lblPersonName" Content="Name:" Height="25" Width="55" Margin="5,0,0,0"/>
        <TextBox x:Name="txBoxPersonName" Height="25" Width="50" Margin="0,0,0,0" Text="{Binding Person.Bind_personNameText, UpdateSourceTrigger=PropertyChanged}"/>
        <!--Second row-->
        <Label x:Name="lblPersonOutput" Content="{Binding Person.Bind_personNameText}" Height="25" Margin="70,0,-59,0" Background="#FFC0F3E9"  />
        </UniformGrid>
      </StackPanel>
    </Grid>
  </GroupBox>
  <GroupBox x:Name="grBoxAnimal" BorderThickness="2" BorderBrush="Blue" Width="160" Height="100" Header="Animal" VerticalAlignment="Center" Canvas.Left="180" Canvas.Top="40" HorizontalAlignment="Left">
   <Grid>
    <StackPanel Orientation="Horizontal" Height="70" Width="135" Margin="0,0,0,0" >
     <UniformGrid Columns="2" Width="135" Margin="5,0,0,0" >
       <!--First row-->
       <Label x:Name="lblAnimalSpecies" Content="Species:" Height="25" Width="55" Margin="5,0,0,0"/>
       <TextBox x:Name="txBoxAnimalSpecies" Height="25" Width="50" Margin="0,0,0,0" Text="{Binding Animal.Bind_animalSpeciesText, UpdateSourceTrigger=PropertyChanged}"/>
       <!--Second row-->
       <Label x:Name="lblAnimalOutput" Content="{Binding Animal.Bind_animalSpeciesText}" Height="25" Margin="70,0,-59,0" Background="#FFC0F3E9" />
     </UniformGrid>
    </StackPanel>
  </Grid>
 </GroupBox>
 <GroupBox x:Name="grBoxCombine" BorderThickness="2" BorderBrush="Orange" Width="330" Height="65" Header="Combine Person and Animal" VerticalAlignment="Top" Canvas.Left="10" Canvas.Top="150" HorizontalAlignment="Center">
  <Grid>
    <StackPanel Orientation="Horizontal" Margin="10,5,10,5">
      <Button x:Name="btnCombine" Width="100" Height="25" Content="Combine" Command="{Binding Combine.Bind_btnCombineCommand}" />
      <Label x:Name="lblCombineOutput" Content="{Binding Combine.Bind_combineCombinedText}" Width="150" Height="25" Margin="30,0,-80,0" Background="#FFC0F3E9"   />
    </StackPanel>
   </Grid>
  </GroupBox>
</Canvas>

Ich weiß, man sollte nicht so viel Code posten. Ich hoffe das war in diesem Fall OK. So haben andere Interessierte auch etwas davon 😊

S
StayCalm Themenstarter:in
8 Beiträge seit 2023
vor 5 Monaten

Vorsetzung, Sorry...

@Th69:

Ich habe es verstanden, dass Du das gleiche meinst, wie „BlonderHans“ in seinem zweiten Punkt.

Ich habe das auch mal so implementiert wie Du es vorgeschlagen hast:

Ich poste hier den Code, so dass andere auch etwas davon haben:

MainViewModel.cs ⇒ NEU/ANGEPASST

namespace Fictiv.ViewModel
{
  public class MainViewModel
  {
    public ViewModelAnimal Animal { get; private set; } = new ViewModelAnimal();
    public ViewModelPerson Person { get; private set; } = new ViewModelPerson();
    //public ViewModelCombine Combine { get; private set; } = new ViewModelCombine();
    public ViewModelCombine Combine { get; init; }

    public MainViewModel()
    {
      Combine = new ViewModelCombine(Person, Animal);
    }
  }
}

ViewModelCombine.cs ⇒ NEU/ANGEPASST

namespace Fictiv.ViewModel
{
  public class ViewModelCombine : ObservableObject
  {
    // the viewmodel have to know the model.
    private readonly Model.Combine CombineModel;

    private readonly ViewModel.ViewModelPerson Person;
    private readonly ViewModel.ViewModelAnimal Animal;


    private string combineCombinedText;
    public string Bind_combineCombinedText
    {
          get { return combineCombinedText; }
          set { SetProperty(ref combineCombinedText, value); }
    }

    // button to combine person with animal text
    public ICommand Bind_btnCombineCommand { get; }

    // Constructor
    public ViewModelCombine(ViewModelPerson person, ViewModelAnimal animal)
    {
          Bind_combineCombinedText = "EMPTY";
          // handle binding methods
          Bind_btnCombineCommand = new RelayCommand(BtnCombineCommand);
          Person = person;
          Animal = animal;
    }

    private void BtnCombineCommand()
    {
      //Bind_combineCombinedText = "Button has been pressed";
                  
      Bind_combineCombinedText = Person.Bind_personNameText;
      Bind_combineCombinedText += " has a ";
      Bind_combineCombinedText += Animal.Bind_animalSpeciesText;
    }
  }
}

Also zusammengefasst, der MessageBus scheint mir, zumindest im Moment als Anfänger, noch etwas aufwendig zu sein.

Bei der Dependecy Injection wird über den Konstruktor das entsprechende ViewModel übergeben. Das finde ich eine charmante Lösung. (Ich denke so ist der Ansatz von Dependency Injection gemeint)

Auf jeden Fall möchte ich mich bei allen für die vielen Vorschläge bedanken. Das hat mir wirklich sehr geholfen, weil das alles noch neu für mich war.

Viele Grüße und noch einen schönen Abend 😊

129 Beiträge seit 2023
vor 5 Monaten

Wenn Person und Animal im Combined als Eigenschaft vorhanden sind, dann braucht man die eigentlich nicht nochmal im MainViewModel. Und wenn du mit einem DI-Container arbeitest, dann brauchst du auch keine Instanzen erzeugen, sondern übergibst die automatisch.

namespace Fictiv.ViewModel
{
  public class MainViewModel
  {
    public ViewModelCombine Combine { get; }

    public MainViewModel( ViewModelCombine combine ) // per DI übergeben
    {
      Combine = combine;
    }
  }
  
  public class ViewModelCombine : ObservableObject
  {
    // the viewmodel have to know the model.
    private readonly Model.Combine CombineModel;

    public ViewModelPerson Person { get; }
    public ViewModelAnimal Animal { get; }

    private string combineCombinedText;
    public string Bind_combineCombinedText
    {
          get { return combineCombinedText; }
          set { SetProperty(ref combineCombinedText, value); }
    }

    // button to combine person with animal text
    public ICommand Bind_btnCombineCommand { get; }

    // Constructor
    public ViewModelCombine(ViewModelPerson person, ViewModelAnimal animal)
    {
          Bind_combineCombinedText = "EMPTY";
          // handle binding methods
          Bind_btnCombineCommand = new RelayCommand(BtnCombineCommand);
          Person = person;
          Animal = animal;
    }

    private void BtnCombineCommand()
    {
      //Bind_combineCombinedText = "Button has been pressed";

      Bind_combineCombinedText = Person.Bind_personNameText;
      Bind_combineCombinedText += " has a ";
      Bind_combineCombinedText += Animal.Bind_animalSpeciesText;
    }
  }
}

BTW: Falls du es mal mit ReactiveUI versuchen möchtest, da sieht so etwas ein wenig cleaner aus

public class MainWindowViewModel : ViewModelBase
{
    public CombinedViewModel Combined { get; }
    public MainWindowViewModel( CombinedViewModel combined )
    {
        Combined = combined;
    }
}

public class CombinedViewModel : ViewModelBase
{
    public AnimalViewModel Animal { get; }
    public PersonViewModel Person { get; }
    public ReactiveCommand<Unit, Unit> CombineCommand { get; }
    [Reactive] public string? Combined { get; private set; }

    public CombinedViewModel( AnimalViewModel animal, PersonViewModel person )
    {
        Animal = animal;
        Person = person;

        var canCombine = this.WhenAnyValue( e => e.Person.Name, e => e.Animal.Species, ( name, species ) => !string.IsNullOrEmpty( name ) && !string.IsNullOrEmpty( species ) );
        CombineCommand = ReactiveCommand.CreateFromTask( OnCombine, canCombine );
    }

    private async Task OnCombine( CancellationToken cancellationToken )
    {
        await Task.Delay( 250 );
        Combined = $"{Person.Name} - {Animal.Species}";
    }
}

public class PersonViewModel : ViewModelBase
{
    [Reactive] public string? Name { get; set; }
}

public class AnimalViewModel : ViewModelBase
{
    [Reactive] public string? Species { get; set; }
}

Und im Anhang das in einem Beispiel-Projekt mit DI-Container usw.

Hat die Blume einen Knick, war der Schmetterling zu dick.

A
764 Beiträge seit 2007
vor 5 Monaten

Du hast ja einen Link bezüglich eines Schichtmodels gepostet. Aber ist das nicht bereits das, was MVVM macht? Auf mich macht es den Eindruck, als wäre das so. Ich kann mich da aber auch irren.

Einfache Antwort: MVVM ist im Schichtenmodel im UI-Teil einzuordnen.

(Natürlich ist das Thema komplexer, aber das spar ich mir hier mal.)

S
StayCalm Themenstarter:in
8 Beiträge seit 2023
vor 5 Monaten

Hallo zusammen.

@Alf:

OK, dann habe ich das wirklich falsch verstanden. Danke nochmal für Deine Rückmeldung.

@BlonderHans:

Danke für Deine Vorschläge. Mich hat Dein Ansatz interessiert und daher wollte ich es zumindest mal nachimplementieren, um zu sehen, wie das Ganze funktioniert.

Der erste Versucht war, die Instanzen automatisch zu übergeben, wie Du es in den ersten Code Schnipseln dargestellt hast. Allerdings klappt das bei mir noch nicht. Zuvor wurde das „MainViewModel“ in der MainWindow.xaml durch den „<Window.DataContext>“ aufgerufen. Jetzt habe ich aber einen Konstruktor, welcher einen Parameter erwartet. Ich weiß jetzt nicht, wie ich diesen neun Konstruktor mit einem Parameter aufrufen kann. Daher bin ich der Meinung, dass ich da noch etwas nicht verstanden habe.

MainWindow.xaml

<Window x:Class="Fictiv.View.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:Fictiv.View"
                xmlns:localVM="clr-namespace:Fictiv.ViewModel" d:DataContext="{d:DesignInstance Type=localVM:MainViewModel}"
                mc:Ignorable="d"
        Title="MainWindow" Height="300" Width="400">

    <Canvas Name="FictivExample" IsEnabled="True" >
        <Label x:Name="lblHedline" Width="160" Height="40" HorizontalAlignment="Left" VerticalAlignment="Center"
                     FontSize="20" FontWeight="Bold" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
                     Canvas.Left="80" Canvas.Top="5"  Content="Fictiv-Example"  />
        <GroupBox x:Name="grBoxPerson" BorderThickness="2" BorderBrush="Green" Width="160" Height="100" Header="Person" VerticalAlignment="Center" Canvas.Left="10" Canvas.Top="40" HorizontalAlignment="Left">
            <Grid>
                <StackPanel Orientation="Horizontal" Height="70" Width="135" Margin="0,0,0,0">
                    <UniformGrid Columns="2" Width="135" Margin="5,0,0,0" >
                        <!--First row-->
                        <Label x:Name="lblPersonName" Content="Name:" Height="25" Width="55" Margin="5,0,0,0"/>
                        <TextBox x:Name="txBoxPersonName" Height="25" Width="50" Margin="0,0,0,0" Text="{Binding Person.Bind_personNameText, UpdateSourceTrigger=PropertyChanged}"/>
                        <!--Second row-->
                        <Label x:Name="lblPersonOutput" Content="{Binding Person.Bind_personNameText}" Height="25" Margin="70,0,-59,0" Background="#FFC0F3E9"  />
                    </UniformGrid>
                </StackPanel>
            </Grid>
        </GroupBox>
        <GroupBox x:Name="grBoxAnimal" BorderThickness="2" BorderBrush="Blue" Width="160" Height="100" Header="Animal" VerticalAlignment="Center" Canvas.Left="180" Canvas.Top="40" HorizontalAlignment="Left">
            <Grid>
                <StackPanel Orientation="Horizontal" Height="70" Width="135" Margin="0,0,0,0" >
                    <UniformGrid Columns="2" Width="135" Margin="5,0,0,0" >
                        <!--First row-->
                        <Label x:Name="lblAnimalSpecies" Content="Species:" Height="25" Width="55" Margin="5,0,0,0"/>
                        <TextBox x:Name="txBoxAnimalSpecies" Height="25" Width="50" Margin="0,0,0,0" Text="{Binding Animal.Bind_animalSpeciesText, UpdateSourceTrigger=PropertyChanged}"/>
                        <!--Second row-->
                        <Label x:Name="lblAnimalOutput" Content="{Binding Animal.Bind_animalSpeciesText}" Height="25" Margin="70,0,-59,0" Background="#FFC0F3E9" />
                    </UniformGrid>
                </StackPanel>
            </Grid>
        </GroupBox>
        <GroupBox x:Name="grBoxCombine" BorderThickness="2" BorderBrush="Orange" Width="330" Height="65" Header="Combine Person and Animal" VerticalAlignment="Top" Canvas.Left="10" Canvas.Top="150" HorizontalAlignment="Center">
            <Grid>
                <StackPanel Orientation="Horizontal" Margin="10,5,10,5">
                    <Button x:Name="btnCombine" Width="100" Height="25" Content="Combine" Command="{Binding Bind_btnCombineCommand, Mode=OneWay}" />
                    <Label x:Name="lblCombineOutput" Content="{Binding Bind_combineCombinedText}" Width="150" Height="25" Margin="30,0,-80,0" Background="#FFC0F3E9"   />
                </StackPanel>
            </Grid>
        </GroupBox>
    </Canvas>
</Window>

MainViewModel.cs

public class MainViewModel
{
    public ViewModelCombine Combine { get; }

    public MainViewModel(ViewModelCombine combine)
    {
        Combine = combine;
    }
}

ViewModelAnimal.cs

public class ViewModelAnimal : ObservableObject
{
	// the viewmodel have to know the model.
	private readonly Model.Animal Animal;

	private string animalSpeciesText;
	public string Bind_animalSpeciesText
	{
		get { return animalSpeciesText; }
		set { SetProperty(ref animalSpeciesText, value); }
	}

	// Constructor
	public ViewModelAnimal() { }
}

ViewModelPerson.cs

public class ViewModelPerson : ObservableObject
{
	// the viewmodel have to know the model.
	private readonly Model.Person Person;

	private string personNameText;
	public string Bind_personNameText
	{
		get { return personNameText; }
		set { SetProperty(ref personNameText, value); }
	}

	// Constructor
	public ViewModelPerson() { }
}

ViewModelCombine.cs

public class ViewModelCombine : ObservableObject
{
	// the viewmodel have to know the model.
	//private readonly Model.Combine CombineModel;

	//private readonly ViewModel.ViewModelPerson Person;
	//private readonly ViewModel.ViewModelAnimal Animal;

	// neu in Fictiv_2
	public ViewModelPerson Person { get; }
	public ViewModelAnimal Animal { get; }


	private string combineCombinedText;
	public string Bind_combineCombinedText
	{
		get { return combineCombinedText; }
		set { SetProperty(ref combineCombinedText, value); }
	}

	// button to combine person with animal text
	public ICommand Bind_btnCombineCommand { get; }

	// Constructor
	public ViewModelCombine(ViewModelPerson person, ViewModelAnimal animal)
	{
		Bind_combineCombinedText = "EMPTY";
		// handle binding methods
		Bind_btnCombineCommand = new RelayCommand(BtnCombineCommand);
		Person = person;
		Animal = animal;
	}

	private void BtnCombineCommand()
	{
		//Bind_combineCombinedText = "Button has been pressed";
		
		Bind_combineCombinedText = Person.Bind_personNameText;
		Bind_combineCombinedText += " has a ";
		Bind_combineCombinedText += Animal.Bind_animalSpeciesText;
	}
}

to be continued...

S
StayCalm Themenstarter:in
8 Beiträge seit 2023
vor 5 Monaten

Vorsetzung letzter Post…

@BlonderHans:

Nach dem ersten Versuch wollte ich es mit der ReactiveUI weiter versuchen. Ich habe die beiden NuGet Pakete „ReactiveUI.Fody“ und „ReactiveUI.WPF“ installiert und meine Klassen erneut angepasst.

MainWindow.xaml

<Window x:Class="Fictiv.View.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:Fictiv.View"
				xmlns:localVM="clr-namespace:Fictiv.ViewModel" d:DataContext="{d:DesignInstance Type=localVM:MainViewModel}"
				mc:Ignorable="d"
        Title="MainWindow" Height="300" Width="400">

	<Canvas Name="FictivExample" IsEnabled="True" >
		<Label x:Name="lblHedline" Width="160" Height="40" HorizontalAlignment="Left" VerticalAlignment="Center"
					 FontSize="20" FontWeight="Bold" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
					 Canvas.Left="80" Canvas.Top="5"  Content="Fictiv-Example"  />
		<GroupBox x:Name="grBoxPerson" BorderThickness="2" BorderBrush="Green" Width="160" Height="100" Header="Person" VerticalAlignment="Center" Canvas.Left="10" Canvas.Top="40" HorizontalAlignment="Left">
			<Grid>
				<StackPanel Orientation="Horizontal" Height="70" Width="135" Margin="0,0,0,0">
					<UniformGrid Columns="2" Width="135" Margin="5,0,0,0" >
						<!--First row-->
						<Label x:Name="lblPersonName" Content="Name:" Height="25" Width="55" Margin="5,0,0,0"/>
						<TextBox x:Name="txBoxPersonName" Height="25" Width="50" Margin="0,0,0,0" Text="{Binding Combine.Person.Name, UpdateSourceTrigger=PropertyChanged}"/>
						<!--Second row-->
						<Label x:Name="lblPersonOutput" Content="{Binding Combine.Person.Name}" Height="25" Margin="70,0,-59,0" Background="#FFC0F3E9"  />
					</UniformGrid>
				</StackPanel>
			</Grid>
		</GroupBox>
		<GroupBox x:Name="grBoxAnimal" BorderThickness="2" BorderBrush="Blue" Width="160" Height="100" Header="Animal" VerticalAlignment="Center" Canvas.Left="180" Canvas.Top="40" HorizontalAlignment="Left">
			<Grid>
				<StackPanel Orientation="Horizontal" Height="70" Width="135" Margin="0,0,0,0" >
					<UniformGrid Columns="2" Width="135" Margin="5,0,0,0" >
						<!--First row-->
						<Label x:Name="lblAnimalSpecies" Content="Species:" Height="25" Width="55" Margin="5,0,0,0"/>
						<TextBox x:Name="txBoxAnimalSpecies" Height="25" Width="50" Margin="0,0,0,0" Text="{Binding Combine.Animal.Species, UpdateSourceTrigger=PropertyChanged}"/>
						<!--Second row-->
						<Label x:Name="lblAnimalOutput" Content="{Binding Combine.Animal.Species}" Height="25" Margin="70,0,-59,0" Background="#FFC0F3E9" />
					</UniformGrid>
				</StackPanel>
			</Grid>
		</GroupBox>
		<GroupBox x:Name="grBoxCombine" BorderThickness="2" BorderBrush="Orange" Width="330" Height="65" Header="Combine Person and Animal" VerticalAlignment="Top" Canvas.Left="10" Canvas.Top="150" HorizontalAlignment="Center">
			<Grid>
				<StackPanel Orientation="Horizontal" Margin="10,5,10,5">
					<Button x:Name="btnCombine" Width="100" Height="25" Content="Combine" Command="{Binding Combine.Bind_BtnCombineCommand, Mode=OneWay}" />
					<Label x:Name="lblCombineOutput" Content="{Binding Combine.Bind_combinedText}" Width="150" Height="25" Margin="30,0,-80,0" Background="#FFC0F3E9"   />
				</StackPanel>
			</Grid>
		</GroupBox>
	</Canvas>
</Window>

MainViewModel.cs

public abstract class ViewModelBase : ReactiveObject
{
	[Reactive] public bool IsInitialized { get; private set; }
	public async Task InitializeAsync(CancellationToken cancellationToken = default)
	{
		IsInitialized = false;
		await OnInitializeAsync(cancellationToken);
		IsInitialized = true;
	}
	protected virtual Task OnInitializeAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

public class MainViewModel : ViewModelBase
{
	public ViewModelCombine Combine { get; }

	public MainViewModel(ViewModelCombine combine)
	{
		Combine = combine;
	}

	protected override async Task OnInitializeAsync(CancellationToken cancellationToken)
	{
		await base.OnInitializeAsync(cancellationToken);
		await Combine.InitializeAsync(cancellationToken);
	}
}

ViewModelAnimal.cs

public class ViewModelAnimal : ViewModelBase
{
	[Reactive] public string? Species { get; set; }

	// Constructor
	public ViewModelAnimal() { }
}

ViewModelPerson.cs

public class ViewModelPerson : ViewModelBase
{
	[Reactive] public string? Name { get; set; }

	// Constructor
	public ViewModelPerson() { }
}

ViewModelCombine.cs

public class ViewModelCombine : ViewModelBase
{
	public ViewModelAnimal Animal { get; }
	public ViewModelPerson Person { get; }

	public ReactiveCommand<Unit, Unit> Bind_BtnCombineCommand { get; }
	[Reactive] public string? Bind_combinedText { get; private set; }

	// Constructor
	public ViewModelCombine(ViewModelPerson person, ViewModelAnimal animal)
	{
		Person = person;
		Animal = animal;

		var canCombine = this.WhenAnyValue(e => e.Person.Name, e => e.Animal.Species, (name, species) => !string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(species));
		Bind_BtnCombineCommand = ReactiveCommand.CreateFromTask(OnCombine, canCombine);
	}

	private async Task OnCombine(CancellationToken cancellationToken)
	{
		await Task.Delay(250);
		Bind_combinedText = $"{Person.Name} - {Animal.Species}";
	}

	protected override async Task OnInitializeAsync(CancellationToken cancellationToken)
	{
		await base.OnInitializeAsync(cancellationToken);
		await Animal.InitializeAsync(cancellationToken);
		await Person.InitializeAsync(cancellationToken);
	}
}

Ich habe mich hier noch zusätzlich etwas an Deinem Beispielprojekt „WpfApp“ orientiert. Allerdings klappt das in meinem Beispiel auch noch nicht. Ich denke ich habe das gleiche Problem wie schon in meinem ersten Versuch. Auch hier wird der Konstruktor nicht aufgerufen.

Wenn ich in Deinem Beispielprojekt einen Breakpoint im Konstruktor „MainViewModel“ platziere, dann scheint mit er so zu sein, als würde diese über die Program.cs, bzw. vermutlich über die App.xaml.cs aufgerufen zu werden? Da bin ich mir aber nicht sicher. Ich habe hier die Vermutung, dass es noch weitere Wege in C# gibt, wie man Konstruktoren, außer über die MainWindow.xaml aufrufen kann. 😊

Dann habe ich noch eine Frage zu Deinem Beispielprojekt. In der „CombineControl.xaml, werden in den „ContentControl“, lediglich „Person“ und „Animal“ gebunden. Woher ist an dieser Stelle klar, dass die Strings „Name“ und „Species“ verwendet werden soll?

Viele Grüße.

129 Beiträge seit 2023
vor 5 Monaten

Mein Beispiel-Projekt setzt voll auf DI und jedes Window wird mittels einem DI-Container erzeugt und der sorgt dafür, dass eben diese Abhängigkeiten (Dependencies) im Konstruktor automatisch übergeben (injected) werden.

Verantwortlich dafür ist Wpf.Extensions.Hosting . Verkabelt wird das in der Program.cs

// Create a builder by specifying the application and main window.
using WpfApp;

var builder = WpfApplication<App, MainWindow>.CreateBuilder( args );

// Configure dependency injection.
// Injecting MainWindowViewModel into MainWindow.
builder.Services
    .AddTransient<MainWindowViewModel>()
    .AddTransient<PersonViewModel>()
    .AddTransient<AnimalViewModel>()
    .AddTransient<CombinedViewModel>()
    ;

var app = builder.Build();

await app.RunAsync();

In WPF kann man für einen Typ ein DataTemplate festlegen und genau das habe ich in der App.xaml gemacht, denn dort angelegt gilt dass dann für die gesamte Anwendung.

        <DataTemplate DataType="{x:Type viewmodels:CombinedViewModel}">
            <views:CombinedControl />
        </DataTemplate>
        <DataTemplate DataType="{x:Type viewmodels:AnimalViewModel}">
            <StackPanel>
                <DockPanel>
                    <Label Content="Species:"
                           DockPanel.Dock="Top" />
                    <TextBox Text="{Binding Species, UpdateSourceTrigger=PropertyChanged}" />
                </DockPanel>
            </StackPanel>
        </DataTemplate>
        <DataTemplate DataType="{x:Type viewmodels:PersonViewModel}">
            <StackPanel>
                <DockPanel>
                    <Label Content="Name:"
                           DockPanel.Dock="Top" />
                    <TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" />
                </DockPanel>
            </StackPanel>
        </DataTemplate>

Ja, in der Beispiel-Anwendung steckt eine ganze Menge an (praktischen) Features. Mir ist es aber auch wichtig zu zeigen, wie diese Features zusammenarbeiten - auch wenn es am Anfang einen erschlagen könnte.

Hat die Blume einen Knick, war der Schmetterling zu dick.