Entwickler-Ecke

WPF / Silverlight - Binding im Datagrid


m.keller - Mo 26.02.18 13:41
Titel: Binding im Datagrid
Hallo,

ich bin gerade dabei eine Benutzerverwaltung auf zu bauen.
Jedem User soll eine Gruppe zugewiesen werden, womit die Zugriffsberechtigung geregelt wird.

Nun habe ich ein Datagrid erstellt. Das war auch soweit kein Problem.
Die Benutzer liegen in einer Collection.
Nun muss ich vorgegebene Gruppen (befinden sich in einer anderen Collection) an eine Combobox binden.

Irgendwie bin ich vermutlich auf dem Holzweg :D

Hier mal das Datagrid:

XML-Daten
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
     <DataGrid VerticalScrollBarVisibility="Auto"
                  HorizontalScrollBarVisibility="Auto"
                  CanUserAddRows="False"
                  CanUserDeleteRows="False"
                  CanUserResizeColumns="False"
                  CanUserReorderColumns="False"
                  CanUserSortColumns="False"
                  AutoGenerateColumns="False"     
                  ItemsSource="{Binding BenutzerVM.BenutzerListe}">
                <DataGrid.CellStyle>
                    <Style TargetType="DataGridCell">
                        <Setter Property="BorderThickness" Value="0"/>
                        <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
                    </Style>
                </DataGrid.CellStyle>
                <DataGrid.RowStyle>
                    <Style TargetType="DataGridRow">
                        <Style.Resources>
                            <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Transparent" />
                            <SolidColorBrush x:Key="{x:Static SystemColors.ControlBrushKey}" Color="Transparent" />
                            <SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="Black" />
                            <SolidColorBrush x:Key="{x:Static SystemColors.ControlTextBrushKey}" Color="Black" />
                        </Style.Resources>
                    </Style>
                </DataGrid.RowStyle>
                <DataGrid.Columns>
                    <DataGridTemplateColumn Header="{glob:Translate Service_BenutzerVerwaltung_Name}" Width="Auto">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <TextBox Text="{Binding Name.Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" IsEnabled="{Binding IsDeleted.Value, Converter={StaticResource BoolInvertConverter}}" />
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                    <DataGridTemplateColumn Header="{glob:Translate Service_BenutzerVerwaltung_Nachname}" Width="Auto">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <TextBox Text="{Binding Vorname.Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" IsEnabled="{Binding IsDeleted.Value, Converter={StaticResource BoolInvertConverter}}"/>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                    <DataGridTemplateColumn Header="{glob:Translate Service_BenutzerVerwaltung_Benutzername}" Width="Auto">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <TextBox Text="{Binding BenutzerName.Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" IsEnabled="{Binding IsDeleted.Value, Converter={StaticResource BoolInvertConverter}}"/>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>

                    <DataGridTemplateColumn Header="{glob:Translate Service_BenutzerVerwaltung_Zugriffsstufe}" Width="Auto">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                   <ComboBox ItemsSource="{Binding BenutzerVM.BenutzerGruppenListe}" DisplayMemberPath="Name.Value"
                                   />
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>                    
                </DataGrid.Columns>
            </DataGrid>


in der Combobox werden Außerhalb des Datagrids die Werte in die Combobox gefüllt.
Nur inerhalb des Grids nicht.


Th69 - Mo 26.02.18 14:36

Gibt es denn Binding-Fehler in der Ausgabe: Gewusst wie: Anzeigen von WPF-Ablaufverfolgungsinformationen [https://msdn.microsoft.com/de-de/library/dd409960.aspx]?


m.keller - Mo 26.02.18 14:39

Oh ja richtig, die hatte ich noch vergessen.

System.Windows.Data Error: 40 : BindingExpression path error: 'BenutzerVM' property not found on 'object' ''BenutzerViewModel' (HashCode=20835089)'. BindingExpression:Path=BenutzerVM.BenutzerGruppenListe; DataItem='BenutzerViewModel' (HashCode=20835089); target element is 'ComboBox' (Name=''); target property is 'ItemsSource' (type 'IEnumerable')

Wobei die Daten Außerhalb des Datagrid ohne Probleme zu finden sind.


Th69 - Mo 26.02.18 16:38

Dann laß mal das BenutzerVM weg (wegen DataItem='BenutzerViewModel'):

XML-Daten
1:
ItemsSource="{Binding BenutzerGruppenListe}"                    


m.keller - Mo 26.02.18 17:30

Das habe ich auch schon versucht.


Palladin007 - Mo 26.02.18 23:34

Also bei WPF (auch liebevoll WTF genannt) sollten mindestens der DebugConverter immer zur Hand sein:


C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
[MarkupExtensionReturnType(typeof(DebugConverter))]
[ValueConversion(typeof(object), typeof(object))]
public class DebugConverter : MarkupExtension, IValueConverter
{

  public IValueConverter RealConverter { get; set; }

  public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
  {
    if (Debugger.IsAttached)
      Debugger.Break();

    return RealConverter?.Convert(value, targetType, parameter, culture) ?? value;
  }

  public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
  {
    if (Debugger.IsAttached)
      Debugger.Break();

    return RealConverter?.ConvertBack(value, targetType, parameter, culture) ?? value;
  }

  public override object ProvideValue(IServiceProvider serviceProvider)
  {
    return this;
  }
}

Bei mir ist das noch etwas umfangreicher. So hab ich noch eine Group-Property (simpel Text, den ich im XAML setze), die mir beim Break hilft, die Converter-Instanz der Position im XAML-Code zuzuordnen, wenn ich den DebugConverter an mehreren Stellen verwende. Daneben habe ich noch eine eine Break-Property, die definiert, ob es wirklich breaken soll. Letzteres ist deshalb praktisch, weil Du dann noch beim Debuggen den DebugConverter sozusagen "aus stellen" kannst ohne das Projekt wieder neu starten zu müssen.

Nutzen kannst Du das, indem Du einfach ein Binding definierst und um den Converter erweiterst:

XML-Daten
1:
ItemsSource="{Binding Converter={debug:DebugConverter}}"                    

Das debug-Namespace musst Du natürlich noch definieren

Sobald WPF das Binding ausführt, führt es auch den Converter aus. Weil kein Path angegeben ist, verwendet es den aktuellen DataContext und dank Debugger.Break() hält er dann genau dort an und Du kannst dir zur Laufzeit anschauen, was für einen Wert WPF in das Binding wirft. Das wird dann vermutlich etwas Anderes sein als Du erwartest.

Beispiel: Die letzte Spalte
Es kann sein (weiß ich beim Standard-DataGrid nicht sicher), dass Du bei der Spalte in einem CellTemplate nicht den selben DataContext hast wie vorher beim DataGrid. Dort hast Du das Objekt als DataContext, was einer Zeile entspricht bzw. ein einzelnes Element aus BenutzerVM.BenutzerListe. Das BenutzerVM.BenutzerGruppenListe gibt's daher nicht, da sich das nur im vorherigen ViewModel, wo es das BenutzerVM.BenutzerListe gibt, befindet.
In diesem Fall musst Du dir mit einer RelativeSource oder einem BindingProxy behelfen, aber dazu mehr, wenn Du weißt, was wirklich der DataContext ist.


m.keller - Di 27.02.18 08:51

Vielen dank für die Ausführliche Erklärung.

Nun habe ich das mal ausgeführt, und es kommt das raus was ich eigentlich schon vermutet habe.
Ich befinde mich in dem Datacontext von der "BenutzerListe"., also in einem der Objecte aus der Collection.

Zitat:
In diesem Fall musst Du dir mit einer RelativeSource oder einem BindingProxy behelfen, aber dazu mehr, wenn Du weißt, was wirklich der DataContext ist.


Das habe ich bis jetzt noch nie verstanden.
Ich hab da bis jetzt auch nie eine sinnvolle Erklärung zu gefunden.
Aber das ist es vermutlich was ich brauche. Denn der Datenkontext muss ein ganz anderer sein. Nicht von den einzelnen Items sondern zwei Schichten da drüber.


m.keller - Di 27.02.18 11:25

So ich habe es nun mit RelativeSource hin bekommen.
Musst etwas ausprobieren, aber durch dein Converter war das dann doch recht simpel.
Danke.

So langsam verstehe ich auch das Prinzip mit dem RelativeSource.


Palladin007 - Di 27.02.18 20:06

Ich persönlich bevorzuge BindingProxies, weil sie flexibler sind und weil mMn. deutlicher erkennbar ist, woher die Quelle kommt.
Das ist die BindingProxy-Klasse, die ich verwende:


C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
public class BindingProxy : Freezable
{
  public static readonly DependencyProperty DataProperty;

  static BindingProxy()
  {
    DataProperty = DependencyProperty.Register(
      nameof(Data),
      typeof(object),
      typeof(BindingProxy),
      new PropertyMetadata(null, (d, e) => ((BindingProxy)d).DataChanged(e)));
  }

  public bool BreakOnChanged { get; set; }

  public object Data
  {
    get => GetValue(DataProperty);
    set => SetValue(DataProperty, value);
  }

  protected override Freezable CreateInstanceCore()
  {
    return new BindingProxy();
  }

  private void DataChanged(DependencyPropertyChangedEventArgs e)
  {
    if (BreakOnChanged && Debugger.IsAttached)
    {
      var oldValue = e.OldValue;
      var newValue = e.NewValue;

      Debugger.Break();
    }
  }
}

Irgendwo, wo der DataContext der ist, den Du brauchst, definierst Du in den Ressource:

XML-Daten
1:
<utils:BindingProxy x:Key="myProxy" Data="{Binding}" />                    

Verwenden tust Du das im Binding dann so:

XML-Daten
1:
ItemsSource="{Binding Data.MyCollectionProperty, Source={StaticResource myProxy}}"                    


Du kannst natürlich auch ein anderes Binding am BindingProxy definieren. Es geht nur darum, dass Du ein Binding für die Data-Property definieren kannst und dann auf den BindingProxy als Ressource zugreifen kannst. Gerade das ist bei z.B. einem ContextMenu sehr hilfreich, denn bei einem ContextMenu würde die RelativeSource nicht ohne Weiteres funktionieren.