I've got a thorny problem and I'm hoping you can help. I'm creating a datagrid which involves dynamically creating columns. Here's some pseudocode for my classes:
GameLibrary
ObservableCollection<Game> Games
Game
ObservableCollection<CustomField> CustomFields
Customfield
ObservableCollection<string> Values
The datagrid is bound to a CollectionViewSource that uses GameLibrary.Games as its Source. The datagrid displays the other properties from Game in each row as I've set up the columns, and then I've got it dynamically created a column for each CustomField in CustomFields and display the relevant CustomField's Values in an itemscontrol in the cell.
This all works great, no problem. Now, though, I'm wanting to sort the Values alphabetically to display. I know best practice for this is using a CollectionViewSource, and I have managed to get one set up, attached to the DataTemplate and displaying in the itemscontrol - but it only works if, as a test, I set the CVS's source to be something external to the datagrid. This displays, but of course it displays the same thing in every row.
How do I bind the DataTemplate's CVS to something in the current row of the table? It's easy enough when not using the CVS, because I can use the binding's Path and just say "CustomFields[i].Values", but I don't know how that translates across to the CVS Source.
Here's what I have now, which works great:
FrameworkElementFactory listbox = new FrameworkElementFactory(typeof(ItemsControl));
Binding b = new Binding();
string pathb = "CustomFields[" + i + "].Values";
b.Path = new PropertyPath(pathb);
b.Mode = BindingMode.TwoWay;
b.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
listbox.SetBinding(ItemsControl.ItemsSourceProperty, b);
listbox.SetValue(ItemsControl.PaddingProperty, new Thickness(5));
dock.AppendChild(listbox);
DataTemplate dt = new DataTemplate { VisualTree = dock };
dt.Seal();
newcolumn.CellTemplate = dt;
gameDataDisplay.Columns.Add(newcolumn);
And here's what I want:
DataTemplate dt = new DataTemplate { VisualTree = dock };
CollectionViewSource listboxCVS = new CollectionViewSource();
SortDescription listboxsortDescription = new SortDescription(".", ListSortDirection.Ascending);
listboxCVS.SortDescriptions.Add(listboxsortDescription);
listboxCVS.Source = SOMETHING HERE BUT I DONT KNOW WHAT;
dt.Resources.Add("customCVS" + i, listboxCVS);
FrameworkElementFactory listbox = new FrameworkElementFactory(typeof(ItemsControl));
Binding b = new Binding();
b.Source = listboxCVS;
listbox.SetBinding(ItemsControl.ItemsSourceProperty, b);
listbox.SetValue(ItemsControl.PaddingProperty, new Thickness(5));
dock.AppendChild(listbox);
dt.Seal();
newcolumn.CellTemplate = dt;
gameDataDisplay.Columns.Add(newcolumn);
I've also tried instead of using a CVS binding to a property in CustomFields that returns a sorted list of the Values and that displays fine, but I know it's not best practice and it doesn't update until you scroll the item offscreen and back, so I think that's a dead end.
Thank you for any help you can offer,
Tom.
PS: The ObservableCollections here aren't strictly ObservableCollections, they're a derived class with a couple extra methods, but they act exactly the same for all practical purposes. Just mentioning here for completeness.
I solved this a different way. Rather than creating a new datatemplate programmatically for each custom column, I defined a datatemplate in the window's resources then used a ContentPresenter to fill in the bindings.
XAML:
<DataTemplate x:Key="customfieldtemplate">
<DockPanel x:Name="customfieldHolder" VerticalAlignment="Center">
<DockPanel.Resources>
<CollectionViewSource x:Key="customfieldview" x:Name="customfieldview" Source="{Binding Values}">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="."/>
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
<CollectionViewSource x:Key="customfieldsview" x:Name="customfieldsview" Source="{Binding PossibleValues}">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="."/>
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
</DockPanel.Resources>
<customcontrols:MultiComboBox Width="17" DockPanel.Dock="Right" Margin="5,0" ShowText="False" x:Name="customfieldMiniCombo" ItemsSource="{Binding Source={StaticResource customfieldsview}}" SelectedItems="{Binding Values}" SelectionMode="Multiple" BorderBrush="{DynamicResource MahApps.Brushes.TextBox.Border}" Background="{DynamicResource MahApps.Brushes.ThemeBackground}" Foreground="{DynamicResource MahApps.Brushes.Text}"/>
<ItemsControl ItemsSource="{Binding Source={StaticResource customfieldview}}" Padding="5"/>
</DockPanel>
</DataTemplate>
Codebehind:
for (int i = 0; i < maindatafile.CurrentGameLibrary.CustomFields.Count; i++)
{
CustomColumn newcolumn = new CustomColumn();
newcolumn.Header = maindatafile.CurrentGameLibrary.CustomFields[i].Name;
gameDataDisplay.Columns.Add(newcolumn);
var template = FindResource("customfieldtemplate");
FrameworkElementFactory factory = new FrameworkElementFactory(typeof(ContentPresenter));
factory.SetValue(ContentPresenter.ContentTemplateProperty, template);
Binding newBinding = new Binding("CustomFields[" + i + "]");
newBinding.Mode = BindingMode.TwoWay;
newBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
factory.SetBinding(ContentPresenter.ContentProperty, newBinding);
DataTemplate dt = new DataTemplate { VisualTree = factory };
newcolumn.CellTemplate = dt;
}
The multicombobox user control (found here) in the template contains an "allowed" set of values to add to or remove from the observablecollection. Its ItemsSource is another property in CustomField, whose getter returns the list of allowable values for that CustomField. The multicombobox's SelectedItems property uses the same binding as the ItemsControl.
End result? The cell displays the list of values in the appropriate CustomField, with a lil dropdown button next to it. That dropdown contains all the possible values for that field, with the ones currently in the list selected. The list updates live as you select and deselect values in this combobox, and also updates live when those values are changed in other parts of the program.
Related
I have ComboBox in a Datagrid that I bind in a .NET WPF application as follows:
<ComboBox x:Name="categoryValues" MinWidth="70"
IsSynchronizedWithCurrentItem="False"
SelectedValuePath="Id"
ItemsSource="{Binding Source={StaticResource categoryViewSource},
Converter={StaticResource CategoriesToCategoriesConverter}}"
SelectedItem="{Binding Category,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged,
Converter={StaticResource CategoryToCategoryConverter}}"
DisplayMemberPath="Name" SelectionChanged="categoryValues_SelectionChanged">
</ComboBox>
So each row is bound to the field Category in a different Model. The categoryViewSource is defined as follows
<CollectionViewSource x:Key="categoryViewSource" d:DesignSource="{d:DesignInstance {x:Type Models:Category}, CreateList=True}"/>
My Category Model implements INotifyPropertyChanged using PropertyChanged.Fody NuGet package. After I load the data from the database using Entity Framework, I assign the data to the CollectionViewSource as follow:
var db = new MyDbContext();
System.Windows.Data.CollectionViewSource categoryViewSource = (System.Windows.Data.CollectionViewSource)this.Resources["categoryViewSource"];
await db.Categories.LoadAsync();
categoryViewSource.Source = db.Categories.Local;
So it's bound to the Observable collection. However, once I do
var c = new Category { Name="New Category" };
db.Categories.Add(c);
my CollectionViewSource is updated (at least in the debugger), which seems logical, it's source is an ObservableCollection. But even if I do
categoryViewSource.View.Refresh();
my ComboBox ItemsSource isn't updated. I tried with IsSynchronizedWithCurrentItem="True" as seen somewhere on StackOverflow. So far it only works, if I do something like this:
categoryViewSource.Source = null;
categoryViewSource.Source = db.Categories.Local;
But then my SelectedItem property is null in all ComboBoxes and they appear empty in the Datagrid because apparently the instances of SelectedItem are different once you reassign the collection.
Does anyone have a solution. Unfortunately, I'm still really new to WPF and have no idea.
Adding a new Category to the DbSet<Category> doesn't affect the ObservableCollection<Category>. These are two different and independent in-memory collections. You will have to add the same object to both Collections.
You could create an ObservableCollection and populate this one with your entity objects when you initialize your ComboBox. And then you add any new objects to both your context and to your data-bound collection, i.e.:
public partial class MainWindow : Window
{
ObservableCollection<Category> _cats;
public MainWindow()
{
InitializeComponent();
var db = new MyDbContext();
System.Windows.Data.CollectionViewSource categoryViewSource = (System.Windows.Data.CollectionViewSource)this.Resources["categoryViewSource"];
await db.Categories.LoadAsync();
_cats = new ObservableCollection<Category>(db.Categories.Local);
categoryViewSource.Source = _cats;
//...
var c = new Category { Name = "New Category" };
db.Categories.Add(c); //add to DbSet
_cats.Add(c); //add to ObservableCollection
}
}
Ok, if I add it directly as ItemsSource, that does work. However if I add the collection through a separate Binding, it doesn't work. I need to pass the collection through the converter though. Is there a way to do it automatically?
You could set up the binding programmatically:
categoryValues.SetBinding(ComboBox.ItemsSourceProperty, new Binding(".") { Source = _cats, Converter = new CategoriesToCategoriesConverter() });
I currently generate my DataGridTextColumns for my DataGrid programmatically. I set their databinding with textColumn.Binding = new Binding(BindingName);. This sets the databinding for the content of that column to a property within the array that the DataGrid's ItemsSource is bound to.
However I also want to bind the DisplayIndex property of the column to properties in my ViewModel.
I'm having a hard time figuring out how to go about setting up the databinding for the DataGridTextColumn's by itself. How do I go about this?
DataGrid:
<DataGrid x:Name="QuickStatsDataGrid"
AutoGenerateColumns="False"
ItemsSource="{Binding QuickStatistics.UserQuickStats}"
Grid.Row="1"
Grid.RowSpan="2"
DockPanel.Dock="Top">
<DataGrid.Columns>
</DataGrid.Columns>
</DataGrid>
The Method that creates the column:
private void AddColumn()
{
DataGridTextColumn textColumn = new DataGridTextColumn();
textColumn.Header = Name;
textColumn.Binding = new Binding(BindingName);
textColumn.MinWidth = 20;
TextColumn = textColumn;
CreatedColumn = true;
DataGridOwner.Columns.Add(textColumn); // Datagrid owner is the DataGrid
}
Edit: The appropriate properties are in my ViewModel. It doesn't seem necessary to post the ViewModel, lets assume there is a property named ColumnDisplayIndex in MyViewModel.
Edit: Addition to my method. The properties in the ViewModel don't seem to update when I change the index programmaticly or by dragging the column over to a new location.
private void AddColumn()
{
DataGridTextColumn textColumn = new DataGridTextColumn();
BindingOperations.SetBinding(textColumn, DataGridColumn.DisplayIndexProperty, new Binding(DisplayIndexBindingName) { Source = viewModel });
textColumn.DisplayIndex = Index;
textColumn.Header = Name;
textColumn.Binding = new Binding(BindingName);
textColumn.MinWidth = 20;
TextColumn = textColumn;
CreatedColumn = true;
DataGridOwner.Columns.Add(textColumn); // Datagrid owner is the DataGrid
}
I have a class called TabViewModel, and it has properties like Name, etc..
I need to be able to add tabs dynamically, and whenever a new tab is added, I need create a new instance of the TabViewModel and bind it to the new tab.
Here's my code:
XAML:
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</TabControl.ItemTemplate>
Code behind:
When adding a new tab..
_tabItems = new List<TabItem>();
tabItem.DataContext = ViewModel.CreateNewTabViewModel();
_tabItems.Add(tabItem);
TabControl1.ItemsSource = _tabItems;
TabControl1.SelectedIndex = 0;
So, CreateNewTabViewModel is suppose to create a new TabViewModel and set the Name property to be displayed on the tab header, which is why the TextBlock is bounded to Name.
I also tried tabItem.SetBinding but it didn't work.
Please advice.
Thanks!
_tabItems = new List<TabItem>();
//...
_tabItems.Add(tabItem);
TabControl1.ItemsSource = _tabItems;
Replaces the entire list of tab items with a new list that contains just a single tab item.
That said, the code is not quite clear on what it is doing, a lot seem unneeded. This works:
var tabItems = new List<TabViewModel>();
tabItems.Add(new TabViewModel { Name = "MyFirstTab" });
myTabControl.ItemsSource = tabItems;
myTabControl.SelectedIndex = 0;
All you need to do is add an instance of a view model to a list of view models and point the tab control to use it. There is no need to set the data context; by setting the items source you are implicitly setting the datacontext of each tab to an item in the collection.
I'm trying to edit the contents of an StackPanel inside a templated DataGrid Column by Code. Unfortunately i cannot find the StackPanel from Code. Can anybody help me, please?
This is my DataTemplate:
<UserControl.Resources>
<DataTemplate x:Key="ReservationContainerTemplate">
<StackPanel Orientation="Horizontal" Background="Black" />
</DataTemplate>
</UserControl.Resources>
This is how I create this Column:
var colReservations = new DataGridTemplateColumn();
colReservations.Header = "Nordplatz";
DataTemplate dt = null;
dt = dataGrid1.FindResource("ReservationContainerTemplate") as DataTemplate;
colReservations.CellTemplate = dt;
dataGrid1.Columns.Add(colReservations);
What I need to do is, writing to this StackPanel inside DataTemplate.
I was able to get the Object by following Code:
DataRowView n = (DataRowView)dataGrid1.Items[i];
//var m = dataGrid1.SelectedItem.Cells[0].Text;
DataTemplate Template = dataGrid1.FindResource("ReservationContainerTemplate") as DataTemplate;
StackPanel stp = Template.LoadContent() as StackPanel;
Great, I've got my Object, but how can I modify it? Under this conditions i've only got a copy of the Object, changes are not reflected to the original one.
Does anybody have got an idea?
I have a button, when pressed it adds a textbox and a listbox to a stackpanel and adds this stackpanel to another stackpanel named "stackPanelAdd". Just like this:
private void buttonAdd_Click(object sender, RoutedEventArgs e)
{
StackPanel sp = new StackPanel();
TextBox tb = new TextBox();
ListBox lb = new ListBox();
tb.Margin = new Thickness(5, 5, 5, 0);
lb.Margin = new Thickness(5, 5, 5, 0);
lb.Height = 200;
sp.Children.Add(tb);
sp.Children.Add(lb);
stackPanelAdd.Children.Add(sp);
}
How do I remove the last children in the stackpanel "stackPanelAdd"?
Should I use something like stackPanelAdd.children.Remove? if so then how do i get the last element in the stackpanel?
Try:
if (stackPanelAdd.Children.Count>0)
{
stackPanelAdd.Children.RemoveAt(stackPanelAdd.Children.Count-1);
}
That is not a good idea, if you stick to this method things will probably get very messy sooner or later. When dealing with items that can be added and removed in WPF you will want to use an ItemsControl of some kind on top of panels (you can change the panel using the ItemsPanel property, by default it will be a StackPanel).
The creation of the controls can also be improved by using data templates and data binding which are core mechanisms that you should become familiar with.
An example:
<ItemsControl ItemsSource="{Binding Data}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBox Text="{Binding Name}" Margin="5,5,5,0"/>
<ListBox ItemsSource="{Binding Items}" Margin="5,5,5,0" Height="200"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Here Data is a source collection which should implement INotifyCollectionChanged, then you can just remove an item from that collection and its corresponding StackPanel will be gone. The items in Data should contain the bound properties Name and Items which you then can assign values to or get entered text from (the class should implement INPC, read more about those things in the article on data binding).
You can use
var lastControl = stackPanelAdd.Children.LastOrDefault();
//Last is defined in System.Linq.Enumrable
if(lastControl != null)
stackPanelAdd.Children.Remove(lastControl);
#Milan Halada's answer worked for me with a little change,
while (stackPanelAdd.Children.Count>0)
{
stackPanelAdd.Children.RemoveAt(stackPanelAdd.Children.Count-1);
}
so, it removes all the children and then i add new children to it dynamically using for loop, with new data.