Binding collection to ComboBox in DataGrid - c#

I have two questions about binding ComboBox to lists objects, when the ComboBox are implemented in DataGrid. But they are so interrelated, that I think two threads are not constructive.
I have a fistful of classes, and I want show their data in a xceed DataGrid. My DataContext is set to ViewModelClass. It has a list of class X objects:
public class ViewModelClass
{
public IList<X> ListX { get; set; }
}
The class X looks something like this. It has a property Id, and a list list of class Y objects.
The list should be my ItemsSource for the ComboBoxes (in DataGrid).
public class X
{
public int Id { get; set; }
// this should be my ItemsSource for the ComboBoxes
public IList<Y> ListY { get; set; }
}
The class Y and Z look something like this. They are some kinds of very simple classes:
public class Y
{
public Z PropZ { get; set; }
}
public class Z
{
public string Name { get; set; }
}
My XAML-Code looks something like this.
<Grid.Resources>
<xcdg:DataGridCollectionViewSource x:Key="ListX" AutoCreateItemProperties="False"
Source="{Binding Path=ListX,
UpdateSourceTrigger=PropertyChanged,
Mode=TwoWay}" />
</Grid.Resources>
<p:DataGrid AutoCreateColumns="False"
ItemsSource="{Binding Source={StaticResource ListX},
UpdateSourceTrigger=PropertyChanged}">
<xcdg:Column Title="Id" FieldName="Id" />
<xcdg:Column Title="Functions" **FieldName="ListY"**>
<xcdg:Column.CellContentTemplate>
<DataTemplate>
<ComboBox DisplayMemberPath="PropZ.Name"
**ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type xcdg:DataGridControl}}, Path=ItemsSource.ListY**}" SelectedValuePath="Funktion.FunktionId" />
</DataTemplate>
</xcdg:Column.CellContentTemplate>
</xcdg:Column>
Now I dont know, how can I bind the ItemsSource of the ComboBox, so that I can read the list values of ListY in my X class?
Then I dont know what is in fact my FieldName for the Functions column?
I entered ListY, because it represents the property (IList<>) in my X class. But I think it is probably not right.
Thanks a lot for your help!

To answer your first question - try this
<ComboBox ItemsSource="{Binding Path=DataContext.ListY,
RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
For your seconds question I am not too sure (it is up to you) but the field name would probably be SelectedFunction or something along those lines

Let's break down your problem into bite sized pieces. You have a ListX collection that is data bound to a DataGrid.ItemsSource property:
<DataGrid ItemsSource="{Binding ListX}" ... />
One thing to note about your code at this stage is that it is pointless setting the Binding.UpdateSourceTrigger property to PropertyChanged on the ItemsSource property. From the linked page:
Bindings that are TwoWay or OneWayToSource listen for changes in the target property and propagate them back to the source. This is known as updating the source. Usually, these updates happen whenever the target property changes. This is fine for check boxes and other simple controls, but it is usually not appropriate for text fields. Updating after every keystroke can diminish performance and it denies the user the usual opportunity to backspace and fix typing errors before committing to the new value. Therefore, the default UpdateSourceTrigger value of the Text property is LostFocus and not PropertyChanged.
You really should know what the code does before you use it.
So anyway, back to your problem... we have a data bound DataGrid and one of its columns has a ComboBox in it. I'm not really sure why you're not using the DataGridComboBoxColumn Class or equivalent, but no matter. Now, you need to understand something about all collection controls:
If a collection of type A is data bound to the ItemsSource property of a collection control, then each item of the collection control will be an instance of type A. This means that the DataContext of each item will be set to that instance of type A. This means that we have access to all of the properties defined in class A from within any DataTemplate that defines what each item should look like.
That means that you have direct access to the ListY property of the X class from within the DataTemplate that defines what your items should look like. Therefore, you should be able to do this:
<DataTemplate>
<ComboBox DisplayMemberPath="PropZ.Name" ItemsSource="{Binding ListY}"
SelectedValuePath="Funktion.FunktionId" />
</DataTemplate>
I can't confirm whether the SelectedValuePath that you set will work, because you didn't mention it anywhere, but if your class Y doesn't have a property named Funktion in it, then it will not work. You'll also have to explain your second problem better, as I didn't really understand it.

I have found a solution, but even that has not proven to be productive. Because the allocation of cell.Content to the comboBox.ItemsSource shows no effect in my View :-(
In XAML, I have the following code
<xcdg:Column.CellContentTemplate>
<DataTemplate>
<p:XDataGridComboBox
DataRow="{Binding RelativeSource={RelativeSource AncestorType={x:Type xcdg:DataRow}}}"
ItemsFieldName="Functions" />
</DataTemplate>
</xcdg:Column.CellContentTemplate>
I have written a custom control in which I explicitly set the data source for each ComboBox:
static XDataGridComboBox()
{
DataRowProperty = DependencyProperty.RegisterAttached(
"DataRow",
typeof(DataRow),
typeof(XDataGridComboBox),
new FrameworkPropertyMetadata(OnChangeDataRow));
ItemsFieldNameProperty = DependencyProperty.RegisterAttached(
"ItemsFieldName",
typeof(string),
typeof(XDataGridComboBox),
new FrameworkPropertyMetadata(OnChangeItemsFieldName));
}
private static void OnChangeDataRow(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var comboBox = d as XDataGridComboBox;
if (comboBox == null)
{
return;
}
var cell =
(from DataCell c in comboBox.DataRow.Cells where c.FieldName == comboBox.ItemsFieldName select c)
.FirstOrDefault();
if (cell == null)
{
return;
}
comboBox.ItemsSource = cell.Content as IEnumerable;
}
The data that I need are available, but the view does not show it. I do not know what I have not considered.

Related

Organize Items in DropDownButton?

I've a collection of items inside an ObservableCollection, each item have a specific nation name (that's only a string). This is my collection:
private ObservableCollection<League> _leagues = new ObservableCollection<League>();
public ObservableCollection<League> Leagues
{
get
{
return _leagues;
}
set
{
_leagues = value;
OnPropertyChanged();
}
}
the League model have only a Name and a NationName properties.
The Xaml looks like this:
<Controls:DropDownButton Content="Leagues" x:Name="LeagueMenu"
ItemsSource="{Binding Leagues}"
ItemTemplate="{StaticResource CombinedTemplate}" >
<Controls:DropDownButton.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding NationName}" />
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</Controls:DropDownButton.GroupStyle>
</Controls:DropDownButton>
but I doesn't get any header for the NationName property, the items inside the DropDown are organized without header but as list, so without organization.
I'm trying to get this predisposition.
What am I doing wrong?
Preliminaries
Grouping items in an ItemsControl in WPF (which DropDownButton derives from) is fairly simple, and is accomplished in two steps. First you need to set up the items source by tweaking an ICollectionView associated with the source collection. Then you need to populate the ItemsControl.GroupStyle collection with at least one GroupStyle item - otherwise the items are presented in a plain (non-grouped) manner.
Diagnosis
The main issue you're facing is getting the drop-down to present the items in a grouped manner. Unfortunately, unlike setting up the items source, it is not something that is easily accomplished in case of the DropDownButton control. The reason for that stems from the way the control (or, more precisely, its template) is designed - the drop-down is presented inside a ContextMenu attached to a Button which is part of the template (see MahApps.Metro source code). Now ContextMenu also derives from ItemsControl, and most of its properties are bound to corresponding properties of the templated DropDownButton. That is however not the case for its GroupStyle property, because it's a read-only non-dependency property, and cannot be bound or event styled. That means that even if you add items to DropDownButton.GroupStyle collection, the ContextMenu.GroupStyle collection remains empty, hence the items are presented in non-grouped manner.
Solution (workaround)
The most reliable, yet most cumbersome solution would be to re-template the control and add GroupStyle items directly to the ContextMenu.GroupStyle collection. But I can offer you a much more concise workaround.
First of all, let's deal with the first step - setting up the items source. The easiest way (in my opinion) is to use CollectionViewSource in XAML. In your case it would boil down to something along these lines:
<mah:DropDownButton>
<mah:DropDownButton.Resources>
<CollectionViewSource x:Key="LeaguesViewSource" Source="{Binding Leagues}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="NationName" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</mah:DropDownButton.Resources>
<mah:DropDownButton.ItemsSource>
<Binding Source="{StaticResource LeaguesViewSource}" />
</mah:DropDownButton.ItemsSource>
</mah:DropDownButton>
Now for the main part - the idea is that we'll create a helper class that will contain one attached dependency property that will assign an owner DropDownButton control to the ContextMenu responsible for presenting its items. Upon changing the owner we'll observe its DropDownButton.GroupStyle collection and use ContextMenu.GroupStyleSelector to feed the ContextMenu with items coming from its owner's collection. Here's the code:
public static class DropDownButtonHelper
{
public static readonly DependencyProperty OwnerProperty =
DependencyProperty.RegisterAttached("Owner", typeof(DropDownButton), typeof(DropDownButtonHelper), new PropertyMetadata(OwnerChanged));
public static DropDownButton GetOwner(ContextMenu menu)
{
return (DropDownButton)menu.GetValue(OwnerProperty);
}
public static void SetOwner(ContextMenu menu, DropDownButton value)
{
menu.SetValue(OwnerProperty, value);
}
private static void OwnerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var menu = (ContextMenu)d;
if (e.OldValue != null)
//unsubscribe from the old owner
((DropDownButton)e.OldValue).GroupStyle.CollectionChanged -= menu.OwnerGroupStyleChanged;
if (e.NewValue != null)
{
var button = (DropDownButton)e.NewValue;
//subscribe to new owner
button.GroupStyle.CollectionChanged += menu.OwnerGroupStyleChanged;
menu.GroupStyleSelector = button.SelectGroupStyle;
}
else
menu.GroupStyleSelector = null;
}
private static void OwnerGroupStyleChanged(this ContextMenu menu, object sender, NotifyCollectionChangedEventArgs e)
{
//this method is invoked whenever owners GroupStyle collection is modified,
//so we need to update the GroupStyleSelector
menu.GroupStyleSelector = GetOwner(menu).SelectGroupStyle;
}
private static GroupStyle SelectGroupStyle(this DropDownButton button, CollectionViewGroup group, int level)
{
//we select a proper GroupStyle from the owner's GroupStyle collection
var index = Math.Min(level, button.GroupStyle.Count - 1);
return button.GroupStyle.Any() ? button.GroupStyle[index] : null;
}
}
In order to complete the second step we need to bind the Owner property for the ContextMenu (we'll use DropDownButton.MenuStyle to do that) and add some GroupStyle items to the DropDownButton:
<mah:DropDownButton>
<mah:DropDownButton.MenuStyle>
<Style TargetType="ContextMenu" BasedOn="{StaticResource {x:Type ContextMenu}}">
<Setter Property="local:DropDownButtonHelper.Owner" Value="{Binding RelativeSource={RelativeSource TemplatedParent}}" />
</Style>
</mah:DropDownButton.MenuStyle>
<mah:DropDownButton.GroupStyle>
<GroupStyle />
</mah:DropDownButton.GroupStyle>
</mah:DropDownButton>
This I think should be enough to achieve your goal.
If you check out the other post you've linked to, the answer has it all - in particular you need to bind to a CollectionView, rather than directly to the collection. Then you can set up grouping on the CollectionView.
So, in your case, define the property:
public ICollectionView LeaguesView { get; private set; }
and then after you've created your Leagues Collection, attach the View to your collection, and while you're at it set up the grouping on the view:
LeaguesView = (ListCollectionView)CollectionViewSource.GetDefaultView(Leagues);
LeaguesView.GroupDesriptions.Add(new PropertyGroupDescription("NationName"));
Then, bind your DropDownButton ItemSource to LeaguesView, and change your HeaderTemplate to bind to "Name" - which is the the name of the group:
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</GroupStyle.HeaderTemplate>
You can also use the ItemCount property in there if you want to show how many items there are in the group.

User Control Child elements sharing Value across multiple instances

I have made a User Control, FontSelector, that groups together a ComboBox for FontFamily Selection and three ToggleButtons for Bold, Italics, Underline options. I am having an issue with the ComboBox's SelectedItem property affecting all instances of that User Control within the same Window. For example, changing the ComboBox selection on one, will automatically change the other. For Clarity. I don't want this behavior. I am very surprised that a User Control is implicitly affecting another User Control.
XAML
<Grid x:Name="Grid" Background="White" DataContext="{Binding RelativeSource={RelativeSource AncestorType=local:FontSelector}}">
<ComboBox x:Name="comboBox" Width="135"
SelectedItem="{Binding Path=SelectedFontFamily}" Style="{StaticResource FontChooserComboBoxStyle}"
ItemsSource="{Binding Source={StaticResource SystemFontFamilies}}"/>
</Grid>
Code Behind
The CLR Property that the ComboBox's SelectedItem is Bound to. Code shown here is in the User Control Code Behind File, not a ViewModel.
private FontFamily _SelectedFontFamily;
public FontFamily SelectedFontFamily
{
get
{
return _SelectedFontFamily;
}
set
{
if (_SelectedFontFamily != value)
{
_SelectedFontFamily = value;
// Modify External Dependency Property Value.
if (value != SelectedFont.FontFamily)
{
SelectedFont = new Typeface(value, GetStyle(), GetWeight(), FontStretches.Normal);
}
// Notify.
RaisePropertyChanged(nameof(SelectedFontFamily));
}
}
}
The Dependency Property that updates it's value based on the Value of the ComboBox's SelectedItem Property. It effectively packages the FontFamily value into a Typeface Object.
public Typeface SelectedFont
{
get { return (Typeface)GetValue(SelectedFontProperty); }
set { SetValue(SelectedFontProperty, value); }
}
// Using a DependencyProperty as the backing store for SelectedFont. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectedFontProperty =
DependencyProperty.Register("SelectedFont", typeof(Typeface), typeof(FontSelector),
new FrameworkPropertyMetadata(new Typeface("Arial"), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
new PropertyChangedCallback(OnSelectedFontPropertyChanged)));
private static void OnSelectedFontPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var instance = d as FontSelector;
var newFont = e.NewValue as Typeface;
if (newFont != null)
{
instance.SelectedFontFamily = newFont.FontFamily;
}
}
EDIT
I think I may have figured out what is going on. I can replicate it by Binding the ItemsSource to the Following Collection View Source.
<CollectionViewSource x:Key="SystemFontFamilies" Source="{Binding Source={x:Static Fonts.SystemFontFamilies}}">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="Source"/>
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
You can then replicate the behavior by placing 2 ComboBoxes and Binding both of them to the CollectionViewSource. They will now, seemingly implicitly track each others SelectedItem. Even without Any Data Binding outside of ItemsSource. It would seem that the CollectionViewSource is somehow playing a part in what the SelectedItem is.
I'd make it a bit different. I'll introduce this solution using only a String, not FontFamily or FontWeight, since I have no VS here right now. (In order to have it working, please change the list of FontFamilies to a list of strings to bind them.)
Your selector UserControl:
- your xaml is ok (but you won't need the x:Name)
- the CodeBehind of the UserControl (later: UC) should change, we will solve it with binding. You should have a DependencyProperty, lets' call it SelectedFontFamily, which will represent the selected string from the ComboBox:
public string SelectedFontFamily
{
get { return (string)GetValue(SelectedFontFamilyProperty); }
set { SetValue(SelectedFontFamilyProperty, value); }
}
public static readonly DependencyProperty SelectedFontFamilyProperty = DependencyProperty.Register("SelectedFontFamily", typeof(string), typeof(YourUC), new PropertyMetadata(string.Empty));
The Window, which contains the UC:
- You should include the namespace of the UC's folder in the opening tag of the window, eg:
<Window
...
xmlns:view="clr-namespace:YourProjectName.Views.UserControls">
- the window's DataContext should have a property with public set option (feel free to implement INotifyPropertyChange on it):
public string FontFamily {get; set;}
- in the Window's xaml you would use the UC this way:
<view:YourUC SelectedFontFamily="{Binding FontFamily, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
It's a two-way binding. You'll find the selected string as the value of the FontFamily property every time you change the SelectedItem.
Edit: you will need View Model class for the Window which is using the UserControl. Create it, make it implement the INotifyPropertyChanged interface, and set it as DataContext for your consumer window. WPF is not like WF, you can find more about it if you Google up "WPF MVVM" or something like that.
Found the problem. I was binding to a CollectionViewSource defined in Application Resources. Until now I was unaware that Binding to a CollectionViewSource will also affect the SelectedItem. The SelectedItem Data gets stored as part of the CollectionViewSource. Setting the IsSynchronizedWithCurrentItem property to False on the ComboBox solved the issue.
Here is an existing answer I have now Found.
Thanks

WPF Combobox selection disappears when the an item is deleted in the list bound to combobox

I have a combo box rigged as
<ComboBox x:Name="HeadComboBox"
ItemsSource="{Binding DataContext.HeadList, RelativeSource={RelativeSource FindAncestor,AncestorType= {x:Type views:FixedAssetBaseWholeUC}}}" Margin="195,78,86,0" VerticalAlignment="Top" SelectedItem="{Binding HeadItem}" DisplayMemberPath="Name" />
The datacontext.HeadList will point to:
public List<FixedAssetHeadItem> HeadList
{
get
{
return _headList;
}
set
{
if (_headList != value)
{
_headList = value;
RaisePropertyChanged("HeadList");
}
}
}
I disable the UserControl in which the combobox rests and load another control to edit the items in the headlist by
DeleteFromHeadList(1);
FixedAssetBaseWholeViewModel fbwvm = (FixedAssetBaseWholeViewModel)Fabwuc.DataContext;
fbwvm.HeadList = HeadList;
When the edit is complete the re enable the usercontrol only to find the selection disappers.
Debug shows
http://postimg.org/image/hdz4h4px3/
How should I deal with this?
You should not bind to List (can cause memory leak), but bind to ObservableCollection<> instead. In this way, your ComboBox should update appropriately. Also your HeadItem should be INPC property - in setter (private or public, depends on your code) should be raising of property changes.

How to get <checkbox> state inside <datagridtemplatecolumn> on any row's checkbox event in datagrid

I have a datagrid - dgUserSession in my WPF application.
It contains 3 columns where only 2 text columns are bound with database and first one is checkbox created manually.
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="False" Name="userSelection" Checked="DataGridCheckBox_Checked" Unchecked="DataGridCheckBox_Unchecked"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.HeaderTemplate>
<DataTemplate>
<CheckBox Checked="chkHeader_Checked" Unchecked="chkHeader_Unchecked" Name="headerSelection">
</CheckBox>
</DataTemplate>
</DataGridTemplateColumn.HeaderTemplate>
</DataGridTemplateColumn>
I want to iterate through each rows state in all four events
private void chkHeader_Checked(object sender, RoutedEventArgs e)
{
for (int i = 0; i < dgUserSession.Items.Count; i++)
{
}
}
But how to get the checkbox is wether checked or not during this iteration?
I spent 3hrs in goggle but no result.
Thanks in advance.
In WPF, it is always best to create your data type classes in the form that is required by the UI. By that, I mean that instead of data binding to two of the three columns in your DataGrid, you should be binding to all three columns. If you add a bool property into the class that is being displayed in each row of the DataGrid, then you can get access to the value of the Checkbox from that class.
If you have not data bound a collection of a custom type to your DataGrid.ItemsSource, then I'd recommend that you do that. Just create a class (that implements the INotifyPropertyChanged interface) and has a property for each column (3 in your case). Then data bind an ObservableCollection of that type to your DataGrid.ItemsSource property:
public ObservableCollection<YourDataType> Items { get; set; }
...
Then in the property setters of your YourDataType class, you will have access to the values of each property/column each time any value is changed:
public someType SomeProperty
{
get { return someProperty; }
set
{
someProperty = value;
NotifyPropertyChanged("SomeProperty");
// each time this is changed, you have access to the other property values here
}
}
You can also iterate through the collection to find the values of the Checkboxes:
foreach (YourDataType item in Items)
{
bool checkBoxValue = item.NewBoolProperty;
}

SelectedItem changing details which are then bound to previous selected item

I have some hierachical data in the form of regions which contain systems which contain entrances
My WPF application has a list of trips which have an entrance as an attribute. The UI is a split window with a ListControl and a 'details' control bound to the ListView.SelectedItem property as follows: (code edited for brevity, only relevant parts shown)
<local:ListView x:Name="listView"/>
<local:DetailsView DataContext="{Binding ElementName=listView, Path=SelectedItem}"/>
The details view consists of ComboBox for the attributes, amongst them:
<ComboBox Name="comboRegion" SelectionChanged="Region_Changed"
ItemsSource="{Binding ElementName=main, Path=Regions, Mode=OneWay}" DisplayMemberPath="Name"
SelectedItem="{Binding Region, Mode=OneWay}"/>
<ComboBox Name="comboSystem" SelectionChanged="System_Changed"
ItemsSource="{Binding ElementName=main, Path=Systems, Mode=OneWay}" DisplayMemberPath="Name"
SelectedItem="{Binding System, Mode=OneWay}"/>
<ComboBox ItemsSource="{Binding ElementName=main, Path=Entrances, Mode=OneWay}" DisplayMemberPath="Name"
SelectedItem="{Binding Enter}"/>
Trip.Enter is the attribute I want to edit, Trip.Region and Trip.System are read only and calculated from Trip.Enter.
main.Regions, main,systems and main.Entrances are lists local to the control which has the following code:
public IEnumerable<Region> Regions { get; private set; }
public IEnumerable<CaveSystem> Systems { get; private set; }
public IEnumerable<Entrance> Entrances { get; private set; }
private void Region_Changed(object sender, SelectionChangedEventArgs e)
{
Region = comboRegion.SelectedItem as Region;
Systems = (region != null ? region.Systems : null);
NotifyPropertyChanged("Systems");
}
private void System_Changed(object sender, SelectionChangedEventArgs e)
{
... Equivalent to Region_Changed except updates Entrances ...
}
The Regions list is static so is populated once.
When a new region is selected, the Systems list is repopulated with the new list
When a new system is selected (also as a cascade from changing region), the entrances list is repopulated.
So far, so good. This works as expected, selecting a trip in the list view binds its details to the combo boxes. Changing the value in a combo box, updates the appropriate "lower level" boxes with the new list.
Finally, selecting an entrance updates the record itself (list view updates appropriately)
The problem is:
When I select a new record in the list view, the values in the new record appear in the combo boxes, but are also copied to the last record selected.
I think the problem is that, changing the data context for the details view causes the bindings to all be updated. I think this happens in turn but changing the selected region causes a ripple effect in the lower level combo boxes which also change. I think they are, at that point, still bound to the old record.
Can anyone suggest a way round this?
You can try use strategy pattern. Dont change your datacontext, but chenge class in your datacontext. In this inner class you can have working logic but your datacontext will be same. Example:
public interface InnerDataContext
{
public CaveSystem System{get;}
}
public class DataContext
{
private InnerDataContext dc;
public CaveSystem
{ get{ return dc.System; }}
}
you can change your inner datacontext when you want and it wouldnt throw update values.
OK, found a solution!
I changed the bindings to
UpdateSourceTrigger=LostFocus
This means that the object is only updated if the user selects a new value in the combo box, Not when the combo box is updated due to changing the data context.

Categories