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() });
Related
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.
I am attempting to bind a list to a combobox. I want to display this list of options within the Combobox itself. (Later to allow the user to select an item 'SelectedItem', I'll cross that bridge when I get there)
MyCode.cs
// List of values for 'Type' dropdown
private static readonly List<string> MarkerTypeList = new List<string>(new string[]
{
"Analog",
"Digital"
});
// Binding for viewing list in window
public List<string> TypeOptions
{
get { return MarkerTypeList; }
}
MyCode.xaml
<ComboBox x:Name="myCombobox" HorizontalAlignment="Left" Margin="125,26,0,0" VerticalAlignment="Top" Width="70" Height="23" SelectedItem="" ItemsSource="{Binding TypeOptions}" />
The solution boiled down to changing this:
ItemsSource="{Binding TypeOptions}"
to this:
ItemsSource="{Binding Marker.TypeOptions}"
Thanks for the input, sorry you didnt have a whole lot to go on.
If you bind you should have your INotifyPropertyChanged interface added to your class which is the actual ViewModel for your ComboBox.
So - 1. Add the Interface to the class. 2. Create RaisePropertyChanged function. 3. Call the function through the setter of the property. This will push the updated value of the property through the binding and you will see the combobox populated.
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 have a ViewModel with two ICollectionViews which are bound as ItemsSources to two different ListBoxes. Both wrap the same ObservableCollection, but with different filters. Everything works fine initially and both ListBoxes appear properly filled.
However when I change an item in the ObservableCollection and modify a property which is relevant for filtering, the ListBoxes don't get updated. In the debugger I found that SourceCollection for both ICollectionVIews is null although my ObservableCollection is still there.
This is how I modify an item making sure that the ICollectionViews are updated by removing and adding the same item:
private void UnassignTag(TagViewModel tag)
{
TrackChangedTagOnCollectionViews(tag, t => t.IsAssigned = false);
}
private void TrackChangedTagOnCollectionViews(TagViewModel tag, Action<TagViewModel> changeTagAction)
{
_tags.Remove(tag);
changeTagAction.Invoke(tag);
_tags.Add(tag);
}
The mechanism works in another context where I use the same class.
Also I realized that the problem disappears if I register listeners on the ICollectionViews' CollectionChanged events. I made sure that I create and modify them from the GUI thread and suspect that garbage collection is the problem, but currently I'm stuck... Ideas?
Update:
While debugging I realized that the SourceCollections are still there right before I call ShowDialog() on the WinForms Form in which my UserControl is hosted. When the dialog is shown they're gone.
I create the ICollectionViews like this:
AvailableTags = new CollectionViewSource { Source = _tags }.View;
AssignedTags = new CollectionViewSource { Source = _tags }.View;
Here's how I bind one of the two (the other one is pretty similar):
<ListBox Grid.Column="0" ItemsSource="{Binding AvailableTags}" Style="{StaticResource ListBoxStyle}">
<ListBox.ItemTemplate>
<DataTemplate>
<Border Style="{StaticResource ListBoxItemBorderStyle}">
<DockPanel>
<Button DockPanel.Dock="Right" ToolTip="Assign" Style="{StaticResource IconButtonStyle}"
Command="{Binding Path=DataContext.AssignSelectedTagCommand, RelativeSource={RelativeSource AncestorType={x:Type tags:TagsListView}}}"
CommandParameter="{Binding}">
<Image Source="..."/>
</Button>
<TextBlock Text="{Binding Name}" Style="{StaticResource TagNameTextBlockStyle}"/>
</DockPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I use MvvmLight's RelayCommand<T> as ICommand implementation in my ViewModel:
AssignSelectedTagCommand = new RelayCommand<TagViewModel>(AssignTag);
I had this issue too, with a similar use-case. When I updated the underlying collection, I would call Refresh() on all the filtered views. Sometimes, this would result in a NullReferenceException thrown from within ListCollectionView.PrepareLocalArray() because SourceCollection is null.
The problem is that you shouldn't be binding to the CollectionView, but to the CollectionViewSource.View property.
Here's how I do it:
public class ViewModel {
// ...
public ViewModel(ObservableCollection<ItemViewModel> items)
{
_source = new CollectionViewSource()
{
Source = items,
IsLiveFilteringRequested = true,
LiveFilteringProperties = { "FilterProperty" }
};
_source.Filter += (src, args) =>
{
args.Accepted = ((ItemViewModel) args.Item).FilterProperty == FilterField;
};
}
// ...
public ICollectionView View
{
get { return _source.View; }
}
// ...
}
The reason for your issue is that the CollectionViewSource is getting garbage collected.
I am trying to bind the result from WCF service to devexpress lookupedit.
this is the property I created
<!-- language: c# -->
public class BindingModel
{
private static List < VW_ClientProcess> _clientProcess= new List< VW_ClientProcess>();
public List< VW_ClientProcess> clientProcess
{
get
{
return _clientProcess;
}
set
{
_clientProcess = value;
OnPropertyChanged("clientProcess");
}
}
}
}
In WPFApp.xaml.cs
BindingModel bind=new BindingModel();
bind.clientProcess = e.Result.GetClientProcessesResult.ToList< VW_ClientProcess>();
this is my xaml code(WPFApp.xaml)
<dxlc:LayoutGroup>
<dxlc:LayoutGroup.DataContext>
<ViewModel:BindingModel />
</dxlc:LayoutGroup.DataContext>
<dxlc:LayoutItem x:Name="liClientProcess"
Width="auto"
VerticalAlignment="Bottom"
Label="Client Process"
LabelPosition="Top">
<dxg:LookUpEdit x:Name="lueClientProcess"
AutoPopulateColumns="True"
DisplayMember="ClientFullName"
ItemsSource="{Binding clientProcess}"
ValueMember="ProcessID" />
</dxlc:LayoutItem>
</dxlc:LayoutGroup>
The problem is when I set ItemSource in xaml, only column names are displaying but the data fields are empty.
but If I set ItemSource through c# code like this
BindingModel bind = new BindingModel();
lueClientProcess.ItemsSource = bind.clientProcess;
lookupedit edit is getting populated. I am new to WPF. I don't know what I am doing wrong here.
It looks like the problem is you are creating duplicate instances of BindingModel. So here in the XAML creates one instance, and assigns it to the view's DataContext:
<dxlc:LayoutGroup.DataContext>
<ViewModel:BindingModel />
</dxlc:LayoutGroup.DataContext>
But then this doesn't use the existing instance, but creates a new one that isn't attached anywhere to the UI:
BindingModel bind=new BindingModel();
bind.clientProcess = e.Result.GetClientProcessesResult.ToList< VW_ClientProcess>();
So I guess what you'd want, instead of the above:
var bind = (BindingModel)DataContext;
bind.clientProcess = e.Result.GetClientProcessesResult.ToList< VW_ClientProcess>();