Laden...

SelectedItem Binding bei dynamisch generiertem UserControl

Erstellt von GeneVorph vor 4 Jahren Letzter Beitrag vor 4 Jahren 1.559 Views
G
GeneVorph Themenstarter:in
180 Beiträge seit 2015
vor 4 Jahren
SelectedItem Binding bei dynamisch generiertem UserControl

Hallo Forum,

vor meiner Frage zunächst euch allen ein gutes neues Jahr 😃

Ich experimentiere derzeit mit zur Runtime erstellten UserControls und habe folgendes Problem:

Mein UserControl besteht aus einem StackPanel in dem sich eine ComboBox und ein Textfeld befinden.

Die Combobox wird per Binding mit Zahlen 1-5 befüllt. Der Index der Combobox ist an ein Property des ViewModels gebunden (TheIndex). Bei jeder Erstellung des UserControls wird dieses Property per Zufallszahl generiert. Obwohl also die ItemsSource immer dafür sorgt, dass die Zahlen 1-5 enthalten sind, soll jedes neu erstellte UserControl zunächst eine andere Zahl anzeigen.

(Damit nur zulässige Indices entstehen, befülle ich die ComboBox mit 5 Zahlen und sorge dafür, dass die Zufallszahl stets zwischen 0 und 5 liegt.)

Dieses UserControl wird per Button-Click auf meinem MainWindow in ein StackPanel geladen.
Bei jedem Button-Click kommt nun auch ein neues UserControl hinzu, jedoch ist der Index immer derselbe (nämlich 0).

Code - UserControl- View


<UserControl x:Class="UserControlTest.PathControl"
             ...
    <UserControl.DataContext>
        <local:ItemsViewModel/>
    </UserControl.DataContext>
    
    <StackPanel Orientation="Horizontal">
        <ComboBox Margin="10" MinWidth="60" VerticalAlignment="Center" ItemsSource="{Binding  AvailableNumbers}" SelectedIndex="{Binding TheIndex}"/>
        <TextBox Margin="10" MinWidth="120"/>
    </StackPanel>
</UserControl>

Code - MainWindow- View


<Window x:Class="UserControlTest.MainWindow"
        ...
    <Window.DataContext>
        <local:ItemsViewModel/>
    </Window.DataContext>
    
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <StackPanel x:Name="HostPanel">
            <ItemsControl ItemsSource="{Binding PathControls}">
                
            </ItemsControl>
            
        </StackPanel>
        <StackPanel Grid.Column="1">
        <Button Command="{Binding UCCreationCommand}" Content="Add User Control" Margin="10"/>
        <Button Command="{Binding UCDeletionCommand}" Content="Delete User Control" Margin="10"/>
        <Button Command="{Binding ChangeIndexCommand}" Content="Change Index" Margin="10"/>
        </StackPanel>
    </Grid>
</Window>

Und hier der Code behind der ItemsViewModel-klasse


 public ObservableCollection<int> AvailableNumbers { get; set; } = new ObservableCollection<int>();
        public ObservableCollection<PathControl> PathControls { get; set; } = new ObservableCollection<PathControl>();

        private int _theIndex;

        public int TheIndex
        {
            get { return _theIndex; }
            set 
            { 
                _theIndex = value;
                OnPropertyChanged(ref _theIndex, value);       
            }
        }

        public ItemsViewModel()
        {
            AvailableNumbers.Add(1);
            AvailableNumbers.Add(2);
            AvailableNumbers.Add(3);
            AvailableNumbers.Add(4);
            AvailableNumbers.Add(5);

            UCCreationCommand = new CommandDelegateBase(UCCreationExecute, UCCreationCanExecute);
            UCDeletionCommand = new CommandDelegateBase(UCDeletionExecute, UCDeletionCanExecute);
            ChangeIndexCommand = new CommandDelegateBase(IndexExecute, IndexCanExecute);
            
        }

        public ICommand UCCreationCommand { get; set; }
        public ICommand UCDeletionCommand { get; set; }
        public ICommand ChangeIndexCommand { get; set; }
        //public event EventHandler<string> OnUCCreationEvent;


        private bool UCCreationCanExecute(object paramerter)
        {
            if (PathControls.Count < 8)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        private void UCCreationExecute(object parameter)
        {           
            PathControl p = new PathControl();          

            PathControls.Add(p);
        }

        private bool UCDeletionCanExecute(object paramerter)
        {
            if (PathControls.Count != 0)
            {
                return true;
            }
            else
            {
                return false;
            }

        }

        private void UCDeletionExecute(object parameter)
        {
            PathControls.RemoveAt(PathControls.Count -1);
        }

         private bool IndexCanExecute(object paramerter)
        {
            return true;
        }

        private void IndexExecute(object parameter)
        {
            Random rnd = new Random();

            int x = rnd.Next(0, AvailableNumbers.Count);

            TheIndex = x;
        }
     
    }

Es ist im Übrigen egal, wann ich TheIndex verändere (d.h. vor oder nach der Erstellung des UserControls) - da passiert nichts, d.h. der Index bleibt 0. Einzig, wenn ich im Konstruktor des ItemsViewModels einen anderen Wert für den Index setze, z. B. 3, dann bekomme ich tatsächlich die 4 angezeigt. Aber auch dann: ändere ich den Index zu einem späteren Zeitpunkt, bleibt alles bei "3".

Noch eine letzte Anmerkung: ich übe noch MVVM und bin mir sicher, dass es nicht das richtige Vorgehen ist, wenn das UserControl aus der ItemsViewModel-Klasse heraus instanziiert wird, sondern ich eine UserControlViewModel-Klasse bräuchte. Ich habe zum jetzigen Zeitpunkt leider keine Ahnung, wie man das bewerkstelligt. Also bitte erst einen Schritt nach dem anderen! Danke.

viele Grüße
Vorph

3.170 Beiträge seit 2006
vor 4 Jahren

Hallo,

das kann so nicht funktionieren.
Du erstellst mehrere ItemsViewModel-Instanzen.
Eines davon ist ans MainWindow gebunden, die anderen jeweils an ein PathControl.

Wenn Du jetzt Deinen Button zum Wechseln des Index klickst, wirkt sich das nur auf den Index des UserControls aus, das ans MainWindow gebunden ist -> deshalb kannst Du in den UsderControls keine Veränderung sehen.
Was soll bei Deinem Code Deiner Meinung nach passieren, wenn Du den Button für das Wechseln des Index klickst, und bereits mehrere UserControls vorhanden sind? Wo genau soll dann gewechselt werden?

Die Vorgehensweise ist schon sehr obskur. Du solltest auf jeden Fall das MainViewModel für das MainWindow von den ItemViewModels trennen - und Dir dann überlegen, welche Funktionalität Du in welchem dieser ViewModels brauchst.

Beim Erstellen eines neuen UserControls solltest Du dann lediglich das ItemsViewModel erzeugen (nicht das Control selbst - das widerspricht MVVM, weil Dein ViewModel Teile der View kennt!) und es in die ObservableCollection packen.
Im ItemsControl des MainWindow legst Du dann ein ItemTemplate an, das Dein UserControl enthält. Such DIr dazu am besten ein paar Beispiele, die mit ItemTemplate arbeiten, es ist nicht sehr kompliziert.

Gruß, MarsStein

Non quia difficilia sunt, non audemus, sed quia non audemus, difficilia sunt! - Seneca

G
GeneVorph Themenstarter:in
180 Beiträge seit 2015
vor 4 Jahren

Danke für die Antwort. Allerdings verstehe ich einige Punkte nicht genau und möchte daher nochmal nachfragen:

Du erstellst mehrere ItemsViewModel-Instanzen.
Eines davon ist ans MainWindow gebunden, die anderen jeweils an ein PathControl.

Stimmt. Und ich weiß, dass im Ernstfall jede View ihr eigenes ViewModel haben sollte. Trotzdem verstehe ich nicht, warum es so nicht funktionieren sollte. Ich habe aber darüber nachgedacht und glaube folgendes Problem ausgemacht zu haben: ich bin bisher der Ansicht, wenn ich im XAML einer view eine local:ItemsViewModel-Variable deklariere, dass sie dann jede View zur genau gleichen Instanz referenziert. Das ist offenbar falsch?

Wenn Du jetzt Deinen Button zum Wechseln des Index klickst, wirkt sich das nur auf den Index des UserControls aus, das ans MainWindow gebunden ist -> deshalb kannst Du in den UsderControls keine Veränderung sehen.

Kapier ich jetzt nicht: welches UserControl habe ich denn ans MainWindow gebunden?

Die Vorgehensweise ist schon sehr obskur.

Das glaube ich dir sehr gerne - genau das passiert beim Lernen: man macht dumme Fehler. Aber die muss man halt erst mal verstehen. Genau deshalb frage ich ja.

Beim Erstellen eines neuen UserControls solltest Du dann lediglich das ItemsViewModel erzeugen (nicht das Control selbst - das widerspricht MVVM, weil Dein ViewModel Teile der View kennt!)

Das wusste ich bereits, aber um ehrlich zu sein, ist mir ein Rätsel, wie das geht. Wie genau erzeuge ich denn das ItemsViewModel, wenn nicht im Code behind? Dazu muss ich doch ins XAML, oder? Aber wie schaffe ich es dann, dass bei jedem Button-Click ein neues kommt? Und wo packe ich das in die von dir erwähnte Observable-Collection? Dazu brauch ich doch ein Objekt der View, oder?

Es würde mich freuen, wenn du mir dazu mal ein Code Beispiel zeigen könntest.

Gruß, Vorph

3.170 Beiträge seit 2006
vor 4 Jahren

Hallo,

ich bin bisher der Ansicht, wenn ich im XAML einer view eine local:ItemsViewModel-Variable deklariere, dass sie dann jede View zur genau gleichen Instanz referenziert. Das ist offenbar falsch?

Ja, das ist falsch. Es wird durch diese Deklaration für jedes UserControl eine eigene Instanz des ViewModels erstellt. Und diese Instanzen wissen eben nicht, dass Du in einer anderen Instanz, die als DataContext im MainWindow lebt, sich irgend eine Index-Property ändert.

Kapier ich jetzt nicht: welches UserControl habe ich denn ans MainWindow gebunden?

Stimmt, mein Fehler... Ich meinte ViewModel, nicht UserControl.

Zum Code:

  1. Nimm aus Deinem UserControl die DataContext-Deklaration raus:
<UserControl x:Class="UserControlTest.PathControl"
             ...
    <StackPanel Orientation="Horizontal">
        <ComboBox Margin="10" MinWidth="60" VerticalAlignment="Center" ItemsSource="{Binding  AvailableNumbers}" SelectedIndex="{Binding TheIndex}"/>
        <TextBox Margin="10" MinWidth="120"/>
    </StackPanel>
</UserControl>
  1. Im MainWindow verpasst Du dem ItemsControl ein ItemTemplate:

            <ItemsControl ItemsSource="{Binding PathViewModels}">
                <ItemsControl.ItemTemplate>
                   <DataTemplate>
                      <local:PathControl> 
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
  1. Trenne die ViewModels auf, ungefähr so (nicht getestet, nur Deinen Code hier im Editor etwas umgebaut, müsste aber grob funktionieren oder zumindest die Vorgehensweise verdeutlichen):
public class PathViewModel
{
        public ObservableCollection<int> AvailableNumbers { get; set; } = new ObservableCollection<int>();

        private int _theIndex;

        public int TheIndex
        {
            get { return _theIndex; }
            set
            {
                _theIndex = value;
                OnPropertyChanged(ref _theIndex, value);
            }
        }

        public PathViewModel()
        {
            AvailableNumbers.Add(1);
            AvailableNumbers.Add(2);
            AvailableNumbers.Add(3);
            AvailableNumbers.Add(4);
            AvailableNumbers.Add(5);
        }
}

public class ItemsViewModel
{
        public ObservableCollection<PathViewModel> PathViewModels { get; set; } = new ObservableCollection<PathViewModel>();

        public ItemsViewModel()
        {
            UCCreationCommand = new CommandDelegateBase(UCCreationExecute, UCCreationCanExecute);
            UCDeletionCommand = new CommandDelegateBase(UCDeletionExecute, UCDeletionCanExecute);
            ChangeIndexCommand = new CommandDelegateBase(IndexExecute, IndexCanExecute);
        }

        public ICommand UCCreationCommand { get; set; }
        public ICommand UCDeletionCommand { get; set; }
        public ICommand ChangeIndexCommand { get; set; }


        private bool UCCreationCanExecute(object paramerter)
        {
            if (PathViewModels.Count < 8)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        private void UCCreationExecute(object parameter)
        {
            PathViewModel p = new PathViewModel();

            PathViewModels.Add(p);
        }

        private bool UCDeletionCanExecute(object paramerter)
        {
            if (PathViewModels.Count != 0)
            {
                return true;
            }
            else
            {
                return false;
            }

        }

        private void UCDeletionExecute(object parameter)
        {
            PathViewModels.RemoveAt(PathViewModels.Count -1);
        }

         private bool IndexCanExecute(object paramerter)
        {
            return true;
        }

        private void IndexExecute(object parameter)
        {
            Random rnd = new Random();

            foreach(var pathVM in PathViewModels)
            {
                int x = rnd.Next(0, AvailableNumbers.Count);
                pathVM.TheIndex = x;
            }
        }
}

So, jetzt erstellst Du mit dem Click auf den Button nur ein ViewModel und packst es in die ObservableCollection. Diese ist an das ItemsControl gebunden - und durch das ItemTemplate wird jetzt für jedes PathViewModel, das sich in der Collection befindet, automatisch ein PathControl angezeigt.

Die ChangeIndex-Geschichte hab ich jetzt mal so umgebaut, dass in jedem vorhandenen PathViewModel ein neuer zufälliger Wert gesetzt wird. Wenn Du das einzeln steuern willst, gehört es aber ins PathViewModel und nicht ins MainViewModel (bzw. ItemsViewModel).

Gruß, MarsStein

Non quia difficilia sunt, non audemus, sed quia non audemus, difficilia sunt! - Seneca

G
GeneVorph Themenstarter:in
180 Beiträge seit 2015
vor 4 Jahren

Vielen Dank, MarsStein - So langsam ergibt das alles Sinn!

Ich habe deinen Code bisher nur gelesen, noch nicht getestet! Ich sehe wohin die Reise geht - wenn es erlaubt ist, noch ein paar Fragen dazu:

Zu Punkt 1: den Datacontext aus dem UserControl nehmen
Die UserControl wird ja wie ein Child behandelt, mit dem MainWindow als Parent, richtig? In diesem Fall, wenn ich den DAtaContext also an der Stelle rausnehme, verwendet das UC den DataContext des MainWindows? Andernfalls würde das Binding ja nicht funktionieren, oder?

Zu Punkt 3: Auftrennen der ViewModels
Kopfzerbrechen bereitet mir die Methode UCCreationExecute. Hier wird ja ein ViewModel (PathViewModel) initiiert. Kennt damit mein ItemsViewModel nicht automatisch mein PathViewModel? Ist das dann MVVM-konform? Oder ist das der Punkt, von dem du meintest

Wenn Du das einzeln steuern willst, gehört es aber ins PathViewModel und nicht ins MainViewModel (bzw. ItemsViewModel).

Falls ja, ist das exakt die Stelle, die ich nicht wirklich verstehe! Das Command wird ja vom MainViewModel (ItemsViewModel) ausgeführt, aber muss doch an der Stelle eine neue Instanz des PathViewModels erstellen, oder? Theoretisch müssten wir aber doch genau an der Stelle ItemsViewModel verlassen und es müsste in PathViewModel weitergehen - oder habe ich dich da falsch verstanden?

viele Grüße
Vorph

3.170 Beiträge seit 2006
vor 4 Jahren

Hallo,

Die UserControl wird ja wie ein Child behandelt, mit dem MainWindow als Parent, richtig? Ja, korrekt.
In diesem Fall, wenn ich den DAtaContext also an der Stelle rausnehme, verwendet das UC den DataContext des MainWindows? Andernfalls würde das Binding ja nicht funktionieren, oder? Generell ist es so, dass Child-Controls den DataContext des Parent automatisch bekommen, sofern man ihn nicht anders besetzt.
Beim ItemsControl ist es aber so, dass jedes darin enthaltene Child-Control als DataContext das jeweilige zugeordnete Item aus der ItemsSource bekommt.
Dadurch werden in Deinem Beispiel automatisch die PathViewModels, die in der (als ItemsSource gebundenen) ObdservableColection stecken, als DataContext für die über das ItemTemplate erstellten Items fungieren. Daher funktioniert das Binding - der DataContext im Item ist immer das zugehörige Objekt aus der ItemsSource.

Kennt damit mein ItemsViewModel nicht automatisch mein PathViewModel? Ist das dann MVVM-konform? Zweimal ja. Typischerweise hat man beim MVVM eine Art Baumstruktur der ViewModels, und es ist völlig in Ordnung, dass übergeorndete ViewModels die untergeordneten ViewModels kennen - das ist sogar meistens notwendig.
Wichtig ist dabei, dass das alles auf ViewModel-Ebene passiert. Gegen MVVM würde es dann verstoßen, wenn (wie in Deinem ursprünglichen Code) in der ViewModel-Ebene das UserControl selbst bekannt sein müsste, da das UserControl eben zur View-Ebene gehört.

Das Command wird ja vom MainViewModel (ItemsViewModel) ausgeführt, aber muss doch an der Stelle eine neue Instanz des PathViewModels erstellen, oder?

Genau, es muss eine Instanz des PathViewModels erstellt werden. Dafür ist aber eben das MainViewModel zuständig, denn es enthält ja die Collection der PathViewModels und muss diese auch pflegen.

Was ich mit "einzeln steuern" meinte, ist die Random-Funktionalität. Wenn Du für jedes PathViewModel einzeln den Index zufällig neu besetzen willst, dann brauchst Du dafür die Funktioaltät im PathViewModel selbst.

Gruß, MarsStein

Non quia difficilia sunt, non audemus, sed quia non audemus, difficilia sunt! - Seneca

G
GeneVorph Themenstarter:in
180 Beiträge seit 2015
vor 4 Jahren

Cool - ich hab's so umgesetzt und hinbekommen 😃
Vielen Dank für deine Erklärungen, MarsStein! Besonders der letzte Post hat nochmal einiges geklärt!
Gruß
Vorph