[WPF] Auswahl in einer Liste ermöglichen
-
Hallo zusammen,
Ich brauche mal ein paar Denkanstösse, wie ich das folgende Problem lösen könnte. Gegeben seien folgende Datenstrukturen:
public class Person { public Guid Id { get; set; } public string Name { get; set; } } class class Group { public Guid Id { get; set; } public string Name { get; set; } public SortedSet<Person> Members { get; private set; } public Group() { this.Members = new SortedSet<Person>(PersonIdComparer.Instance); } } // irgendwo: List<Group> groups = new List<Group>(); // usw.
Jetzt habe irgendwo ein Objekt von der Klasse
Person
.Name
wird an eine Textbox gebunden, usw. Ich will nun aber dort auch die Möglichkeit bieten, die Mitgliedschaften in den verschiedenen Gruppen zu editieren. Im Code kann ich leicht eine Liste von Gruppen erstellen, wo die Person dabei ist, aber wie kann ich in WPF mit Binding Mitteln dies bewerkstelligen? Am liebsten würde ich es über eine ComboBox machen, wo man mehrere Einträge Selektieren kann und im Text dann eine Komma-Liste der Gruppennamen angezeigt wird.
Zu Multi-Selection-Combobbox findet man zahlreiche Einträge im Inet, doch die haben alle so wunderschöne Datenstrukturen im Hintergrund, welche sich ideal anbinden lassen, also Listen mit Einträgen, welche jeweils zwei Properties haben:IsSelected
undName
.
Soll ich nun am besten extra eine neue Liste mit neuen Objekten erstellen, damit ich dies auch so ideal anbinden kann? Und wie krieg ich die Auswahl wieder zurück?Wenn ich das Problem noch eine Ebene komplizierter machen darf: Wenn ich diese Multi-Selection-ComboBox dann in ein WPF DataGrid werfe, wie kann ich dann die Umwandlung der Liste durchführen? Über einen Converter? Doch wo sollte ich den idealerweise hinsetzen? Beim Anbinden an ItemSource der ComboBox in der DataGridTemplateColumn? Und wie soll es dann wieder zurück gehen, also dass die
Group
Objekte entsprechend der Auswahl verändert werden?Vielen Dank im voraus.
Grüssli
-
Ich versteh nicht ganz was du erreichen Möchtest.
- Möchtest du die User anzeigen und dessen Gruppen verwalten?
- Möchtest du Gruppen anzeigen und dessen User verwalten?
- Möchtest du einfach Benutzer nach Gruppen Sortieren?Was für eine UI schwebte dir vor?
-
David W schrieb:
Möchtest du die User anzeigen und dessen Gruppen verwalten?
Ja.
David W schrieb:
Möchtest du Gruppen anzeigen und dessen User verwalten?
Nein, bzw. in der Zukunft vielleicht oder nur anzeigen, sehe ich aber aktuell als das kleinere Problem.
David W schrieb:
Möchtest du einfach Benutzer nach Gruppen Sortieren?
Nein, dazu bräuchte ich ja kein WPF. Kann ich schnell mit Linq erreichen.
David W schrieb:
Was für eine UI schwebte dir vor?
Stell dir ein DataGrid vor.
Jede Reihe im DataGrid stellt eine Person dar.
Neben den üblichen Kolonnen für Namen usw., gibt es eine Kolonne für Gruppenzugehörigkeit.
Diese Gruppenzugehörigkeitskolonne soll eine ComboBox sein.
Wenn man die ComboBox aufklappt, kann man die Gruppen selektieren, zu welcher die Person gehört.Dies grenzt ja an die Nähe der Unmöglichkeit in WinForms und WPF ist doch so schön erweiterbar, da dachte ich, dass dies doch kein grosses Problem in WPF sein müsste. Aktuell habe ich es nun doch über einen Dialog gemacht und auf ein flaches GUI in dem Bereich verzichtet. Aber das hätte ich auch mit WinForms erreicht.
Grüssli
-
Also ich denk über eine MultiSelectComboBox kommst du nicht herum, du sagtest du hättest schon welche gefunden. Musst dann nur noch eine Wrapper Klassen haben, eine für die Gruppe die ein "IsChecked" anbietet, und eine die Alle gruppen kennt und die gecheckten und ungecheckten Gruppen lesen und setzen kann.
Aber mir kommt da grad ne idee.
Man könnte sich ein Popup bauen und direkt unter der Zelle anzeigen (wie eine ComboBox es machen würde)<Popup> <DockPanel> <StackPanel Orientation="Horizontal"> <Button Content="OK" /> <Button Content="Cancel" /> </StackPanel> <ListBox ItemsSource="alle gruppen elemente irgendwoher" /> </DockPanel> </Popup>
Und sobald man auf OK klickt holt man sich die Selected Items. Es ist praktisch wie ein kleines Child Window was in einer "Fake ComboBox" angezeigt wird.
Müsste man mal probieren.
-
David W schrieb:
Also ich denk über eine MultiSelectComboBox kommst du nicht herum, du sagtest du hättest schon welche gefunden. Musst dann nur noch eine Wrapper Klassen haben, eine für die Gruppe die ein "IsChecked" anbietet, und eine die Alle gruppen kennt und die gecheckten und ungecheckten Gruppen lesen und setzen kann.
Also würdest du vorschlagen Wrapperklassen zu erstellen, welche die Daten zusammenstellen? Wodurch man immer wieder eine neue Liste mit Wrapperklassen-Objekten erstellen müsste, wenn man die Gruppenzugehörigkeit editieren möchte? Dann mit einem Converter an die ItemSource binden, wobei im Converter die neue Liste von Objekten erstellt wird? Und bei der Rückführung ... hmmm
Ich möchte möglichst auf Buttons wie Ok oder Cancel verzichten. Häckchen rein und somit ist auch die Zuordnung bereits getätigt. Ok oder Cancel sind da nur unnötige zusätzliche Schritte.Müsste man wohl irgendwie auf das CheckBox-Checked Event reagieren. Geht das aus einem DataTemplate? Bin mir grad nicht mehr sicher, muss ich später dann mal probieren.
Grüssli
-
Also Wrapper klassen wären schon angebracht, die können ja nur für die View existieren.
In MVVM umgebungen macht man das ja auch so, das man die Daten so aufbereitet das sie für jegliche art von View konsumiert werden können.Was den OK Button an geht, da ist halt das Problem wann das Popup (oder die ComboBox) zu gehen soll, bei einem klick auf einer CheckBox? Wär doof wenn man da viel klicken muss wenn man mehrere haben möchte Aufklappen->Auswählen->Geht zu->Aufklappen->Auswählen....
Ich glaub manche machen das so das man die ComboBox manuell wieder zu klappen muss. Ob es dann ein OK Button ist der der kleine nach unten Zeigende Pfeil ist egal, ist nur ne andere position und beschriftung.Auf Events in Xaml reagieren kannst du nur in Form von EventTriggers, wie ich in den anderen Thread schon schrieb ist Xaml ja eine Deklarative sprache, eine Methode kann man nicht ausführen.
Also um C# Code kommt man da nicht herum denk ich.Man könnte sich auch was mit der normalen ComboBox basteln, habe da ma was aus den Boden gestampft:
(habe vor 21 Min das Projekt erstellt)Das Fenster:
<Window x:Class="WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Local="clr-namespace:WpfApplication1" Title="MainWindow" Height="350" Width="525"> <DockPanel> <ComboBox Local:GroupsBehavior.ItemsSource="{Binding Groups}" DockPanel.Dock="Top" HorizontalAlignment="Right" MinWidth="150" IsEditable="True"> <ComboBox.ItemTemplate> <DataTemplate> <CheckBox IsChecked="{Binding IsChecked}" Content="{Binding Name}" /> </DataTemplate> </ComboBox.ItemTemplate> </ComboBox> <Grid /> </DockPanel> </Window>
Code Behind:
using System.Collections.Generic; using System.Windows; namespace WpfApplication1 { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); DataContext = this; Groups = new List<Group>(); Groups.Add(new Group("First")); Groups.Add(new Group("Second")); Groups.Add(new Group("Third")); } public List<Group> Groups { get; set; } } }
Behavior:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; namespace WpfApplication1 { public class GroupsBehavior : DependencyObject { public static List<Group> GetItemsSource(DependencyObject obj) { return (List<Group>)obj.GetValue(ItemsSourceProperty); } public static void SetItemsSource(DependencyObject obj, List<Group> value) { obj.SetValue(ItemsSourceProperty, value); } public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.RegisterAttached("ItemsSource", typeof(List<Group>), typeof(GroupsBehavior), new UIPropertyMetadata(OnItemsSourceChanged)); private static void OnItemsSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) { var wrapper = new List<GroupWrapper>(); var comboBox = (ComboBox)sender; var groups = e.NewValue as List<Group>; if (groups != null) { comboBox.DropDownClosed += new EventHandler(ComboBox_DropDownClosed); wrapper.AddRange(groups.Select(g => new GroupWrapper { Group = g, Name = g.Name })); comboBox.ItemsSource = wrapper; comboBox.Tag = wrapper; } } // TODO: Vorselektieren der Items beim DropDownOpen private static void ComboBox_DropDownClosed(object sender, EventArgs e) { var comboBox = (ComboBox)sender; var builder = new StringBuilder(); comboBox.SelectedItem = null; foreach (var item in comboBox.ItemsSource) { var wrapper = item as GroupWrapper; if (wrapper != null && wrapper.IsChecked) builder.AppendFormat("{0}, ", wrapper.Name); } comboBox.Text = builder.ToString(); } } public class GroupWrapper { public Group Group { get; set; } public string Name { get; set; } public bool IsChecked { get; set; } } }
Ich habe mir nur das PropertyChanged gerade gespart.
Es ist auch noch nicht so schick, man könnte das in einem Custom Control verpacken und richtig machen
Ein Custom Control der
- von der ComboBox ableitet
- das Style implementiert
- ein weiteres "ItemsSource" anbietet wie mein behavior das tut
- und dann noch ein TextBlock im Template sodass man auf das "IsEditable" verzichten kann.Also am Ende würde ich es so machen:
- Eine Liste von allen Gruppen und Personen bei dem Fenster im Code Behind (Oder MainViewModel ^^)
- Eine Liste von Personen wo es eine Spalte mit "GroupComboBox" objekten gibt
- Diese "GroupComboBox" hat ein weiteres "GroupItemsSource" property
- Man bindet dieses "GroupItemsSource" gegen die Gruppen im Fenster (RelativeSource)
- Initial wird dann die Wrapper Klasse auf das richtige ItemsSource gebunden
- Die Gruppen werden dann vor selektiert anhand der Person (Die Person ist dann im DataContext der "GroupComboBox")
- Sobald die DropDown sich schließt, werden die Selektierten Gruppen geholt und die Person gesetzt
(Initial und beim DropDownClose wird auch der Text gesetzt, evtl ein Seperator Property im Control)
-
Danke erstmal für die Anregung.
Auf Events in Xaml reagieren kannst du nur in Form von EventTriggers, wie ich in den anderen Thread schon schrieb ist Xaml ja eine Deklarative sprache, eine Methode kann man nicht ausführen.
Also um C# Code kommt man da nicht herum denk ich.Das ist mir schon klar, ich meinte auch, dass man auf das Checked-Event im C# Code reagieren soll. Gibt ein paar Implementationen im Inet, welche dies so handhaben. Deine Vorgehensweise habe ich jetzt noch nie gesehen, ist aber auch interessant und lernreich.
Ich werde mich die Tage noch damit beschäftigen und melde mich dann zurück. Was mir aktuell extrem missfällt sind all diese Wrapperklassen. Vor allem muss man eine riesige Anzahl solcher Objekte erstellen. Wenn man sich überlegt, wenn ich eine Liste von z.B. 200 Personen habe und die Liste der Gruppen 20 beträgt. Dann muss ich, wenn ich alle Personen im DataGrid oder sonstwo anzeigen lassen möchte, 20 * 200 = 4000 GroupWrapper Objekte erstellen. Das kann es doch nicht sein ...
Grüssli
-
Wenn die Anzahl der Items doch von Relevanz wird, zb wenn der Speicher knapp ist, dann kann man das eventuell auch Lazy implementieren. Zb das die eben aufgezeigten Wrapper Klassen erst erzeugt werden beim ersten aufklappen der ComboBox.
Ich denk aber nicht das dass so tragisch ist, denn diese Wrapper klassen sind wirtklich extrem klein.
Was die Anzahl an geht, da kann man sich eventuell Generische Klassen ausdenken. Man hat da freie hand.
Dier Lösung die ich da vor schlug hat eher den Aspekt das man die Original Daten (Personen, Gruppen) nicht mit sachen verschmutzt die man nur in der View braucht.
-
So, hatte grad ma langeweile:
Generic.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Local="clr-namespace:WpfApplication1"> <Style TargetType="{x:Type Local:GroupComboBox}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Local:GroupComboBox}"> <Grid> <ComboBox x:Name="PART_ComboBox" ItemsSource="{Binding Wrappers, RelativeSource={RelativeSource AncestorType={x:Type Local:GroupComboBox}}}"> <ComboBox.ItemTemplate> <DataTemplate> <CheckBox IsChecked="{Binding IsChecked}" Content="{Binding Name}" /> </DataTemplate> </ComboBox.ItemTemplate> </ComboBox> <TextBlock Text="{Binding DisplayText, RelativeSource={RelativeSource AncestorType={x:Type Local:GroupComboBox}}}" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="0,0,20,0" /> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
GroupComboBox.cs
using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Windows; using System.Windows.Controls; namespace WpfApplication1 { [TemplatePart(Name = "PART_ComboBox", Type = typeof(ComboBox))] public class GroupComboBox : Control { static GroupComboBox() { DefaultStyleKeyProperty.OverrideMetadata(typeof(GroupComboBox), new FrameworkPropertyMetadata(typeof(GroupComboBox))); } public GroupComboBox() { Wrappers = new ObservableCollection<GroupWrapper>(); } public List<Group> ItemsSource { get { return (List<Group>)GetValue(ItemsSourceProperty); } set { SetValue(ItemsSourceProperty, value); } } public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(List<Group>), typeof(GroupComboBox), new UIPropertyMetadata(null)); public string Separator { get { return (string)GetValue(SeparatorProperty); } set { SetValue(SeparatorProperty, value); } } public static readonly DependencyProperty SeparatorProperty = DependencyProperty.Register("Separator", typeof(string), typeof(GroupComboBox), new UIPropertyMetadata(", ")); public string DisplayText { get { return (string)GetValue(DisplayTextProperty); } set { SetValue(DisplayTextProperty, value); } } public static readonly DependencyProperty DisplayTextProperty = DependencyProperty.Register("DisplayText", typeof(string), typeof(GroupComboBox), new UIPropertyMetadata("")); public ObservableCollection<GroupWrapper> Wrappers { get { return (ObservableCollection<GroupWrapper>)GetValue(WrappersProperty); } set { SetValue(WrappersProperty, value); } } public static readonly DependencyProperty WrappersProperty = DependencyProperty.Register("Wrappers", typeof(ObservableCollection<GroupWrapper>), typeof(GroupWrapper), new UIPropertyMetadata(null)); public override void OnApplyTemplate() { base.OnApplyTemplate(); var combobox = GetTemplateChild("PART_ComboBox") as ComboBox; combobox.DropDownOpened += new System.EventHandler(Combobox_DropDownOpened); combobox.DropDownClosed += new System.EventHandler(Combobox_DropDownClosed); combobox.SelectionChanged += new SelectionChangedEventHandler(Combobox_SelectionChanged); } private void Combobox_SelectionChanged(object sender, SelectionChangedEventArgs e) { var comboBox = (ComboBox)sender; comboBox.SelectedItem = null; } private void Combobox_DropDownOpened(object sender, System.EventArgs e) { // TODO: Preselect //var person = DataContext as Person if (Wrappers.Count == 0) { foreach (var item in ItemsSource) { Wrappers.Add(new GroupWrapper { Group = item, Name = item.Name }); } } } private void Combobox_DropDownClosed(object sender, System.EventArgs e) { DisplayText = string.Join(Separator, Wrappers.Where(w => w.IsChecked).Select(w => w.Name)); } } public class GroupWrapper : DependencyObject { public Group Group { get; set; } public string Name { get; set; } public bool IsChecked { get; set; } } }
MainWindow.xaml
<Window x:Class="WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Local="clr-namespace:WpfApplication1" Title="MainWindow" Height="350" Width="525"> <DockPanel> <Local:GroupComboBox ItemsSource="{Binding Groups}" MinWidth="150" DockPanel.Dock="Top" HorizontalAlignment="Right" /> <Grid /> </DockPanel> </Window>
MainWindow.xaml.cs
using System.Collections.Generic; using System.Windows; namespace WpfApplication1 { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); DataContext = this; Groups = new List<Group>(); Groups.Add(new Group("First")); Groups.Add(new Group("Second")); Groups.Add(new Group("Third")); } public List<Group> Groups { get; set; } } }
Sieht nun schon etwas schicker aus
- Die ComboBox ist nun read only
- Das "SelectedItem" blinkt nicht mehr auf
- Die Items werden beim ersten aufklappen erst erzeugt
- DisplayText wird nun zur anzeige der Gesetzten items verwendet
- Die MainWindow ist kürzer und klarerTODO:
Initiales setzen von DisplayText beim Landen der Personen aus der Hauptapplikation heraus (Anzeige der gesetzten Gruppen)
Initiale vorauswahl der Gruppen bei dem erstellen der Wrapper ItemsWenn du drüber schaust siehst du, alles sehr einfach und deutlich.
-
Nochmals danke. Wie gesagt, ich werde es mir die Tage mal anschauen.
Die Grösse der Wrapperklassen ist definitiv nicht das Problem. Das Problem stellt eher dasnew
davor dar. In kleinerem Umfang macht es auch noch nichts aus, aber wenn die Anzahl zusätzlicher Objekte so exponential wächst ...
Allerdings habe ich vielleicht bereits eine Idee, wie man die Anzahl schwer reduzieren könnte. Muss ich aber zuerst noch genau zueende denken.Grüssli
-
Das Problem der sehr vielen Elemente hast du in der UI sowieso. Zb in ItemControls (ListBox, ComboBox usw), da wird der ItemContainer auch immer erstellt. Wenn mann dann noch viele Panels, nested Controls usw erstellt wächst es sehr schnell.
Mit dem Virtualisieren kann WPF dem aber gut entgegenwirken