I have a simple class as defined below:
public class Person
{
int _id;
string _name;
public Person()
{ }
public int ID
{
get { return _id; }
set { _id = value; }
}
public string Name
{
get { return _name; }
set { _name = value; }
}
}
that is stored in a database, and thru a bit more code I put it into an ObservableCollection object to attempt to databind in WPF later on:
public class People : ObservableCollection<Person>
{
public People() : base() { }
public void Add(List<Person> pListOfPeople)
{
foreach (Person p in pListOfPeople) this.Add(p);
}
}
In XAML, I have myself a ListView that I would like to populate a ListViewItem (consisting of a textblock) for each item in the "People" object as it gets updated from the database. I would also like that textblock to bind to the "Name" property of the Person object.
I thought at first that I could do this:
lstPeople.DataContext = objPeople;
where lstPeople is my ListView control in my XAML, but that of course does nothing. I've found TONS of examples online where people through XAML create an object and then bind to it through their XAML; but not one where we bind to an instantiated object and re-draw accordingly.
Could someone please give me a few pointers on:
A) How to bind a ListView control to my instantiated "People" collection object?
B) How might I apply a template to my ListView to format it for the objects in the collection?
Even links to a decent example (not one operating on an object declared in XAML please) would be appreciated.
You were so close.
lstPeople.ItemsSource = objPeople; // :)
The only other thing you need is how to apply a view for each item in your collection. No problem. I won't use a ListView... I'll just use an ItemsControl because it's a bit simpler.
<ItemsControl x:Name="lstPeople">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
That's pretty much it. The strategy is the same for a Listview, but you need to provide just a tad more XAML to provide column headers and stuff. I'm not 100% sure you need this at the moment, so I left it out.
Edit: Here's an extension method for "AddRange" that will do what you are trying to do by subclassing ObservableCollection. Little easier... especially if you end up with a lot of collection types (you will)
public static void AddRange<T>(this ObservableCollection<T> collection, IEnumerable<T> items)
{
foreach(var item in items)
{
collection.Add(item);
}
}
Then you can just do:
ObservableCollection<Person> peeps = new ObservableCollection<Person>();
peeps.AddRange(new List<Person>
{
new Person() { Name = "Greg" },
new Person() { Name = "Joe" }
});
#Anderson beat me by seconds, my answer was very similar.
One thing i will add though: there is no need to define a new type of collection that inherits from ObservableCollection, instead you can just do this:
ObservableCollection<Person> myPeopleCollection = new ObservableCollection<Person>();
the only time you want to extend it is if you are going to be doing something different or fancy with it, which you don't appear to be doing.
Indeed 'Anderson Imes' response is correct (upvote), although I have two remarks:
First, if you only need to display one property of an object, I think it's easier and cleaner to use ListBox instead of ListView, because the code for binding the property will be reduced to
<ListBox x:Name="lstPeople" DisplayMemberPath="Name" />
Second, if you are using WPF and Binding, make sure your objects implement INotifyPropertyChanged, so that changes are always synchronized between UI and the objects.
public class Person : INotifyPropertyChanged
{
int _id;
string _name;
public Person()
{ }
public int ID
{
get { return _id; }
set {
_id = value;
RaisePropertyChanged("ID");
}
}
public string Name
{
get { return _name; }
set {
_name = value;
RaisePropertyChanged("Name");
}
}
pivate void RaisePropertyChanged(string propName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
Related
I have a strange use case for WPF DataGrid using MVVM through ReactiveUI that doesn't quite fit any other solution I've found so far.
The Problem Set
I have a DataSet that contains a list of Users. Each User has a string Id and a set of uniquely-identified data fields associated with it that can be represented as a set of string key-value pairs. All Users within a DataSet will have the same set of fields, but different DataSets may have different fields. For example, all Users in one DataSet may have fields "Name", "Age", and "Address"; while Users in another DataSet may have fields "Badge #" and "Job Title".
I would like to present the DataSets in a WPF DataGrid where the columns can be dynamically populated. I would also like to add some metadata to fields that identify what type of data is stored there and display different controls in the DataGrid cells based on that metadata: Pure text fields should use a TextBox, Image filepath fields should have a TextBox to type in a path and a Button to bring up a file-select dialog, etc.
What I Have That Works (but isn't what I want)
I break my data up into ReactiveUI ViewModels. (omitting RaisePropertyChanged() calls for brevity)
public class DataSetViewModel : ReactiveObject
{
public ReactiveList<UserViewModel> Users { get; }
public UserViewModel SelectedUser { get; set; }
};
public class UserViewModel : ReactiveObject
{
public string Id { get; set; }
public ReactiveList<FieldViewModel> Fields { get; }
public class FieldHeader
{
public string Key { get; set; }
public FieldType FType { get; set; } // either Text or Image
}
public ReactiveList<FieldHeader> FieldHeaders { get; }
};
public class FieldViewModel : ReactiveObject
{
public string Value { get; set; } // already knows how to update underlying data when changed
}
I display all of this in a DataSetView. Since Id is always present in Users, I add the first DataGridTextColumn here. Omitting unnecessary XAML for more brevity.
<UserControl x:Class="UserEditor.UI.DataSetView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:UserEditor.UI"
x:Name="DataSetControl">
<DataGrid Name="UserDataGrid"
SelectionMode="Single" AutoGenerateColumns="False"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
DataContext="{Binding Path=ViewModel.Users, ElementName=DataSetControl}">
<DataGrid.Columns>
<DataGridTextColumn Header="Id" Binding="{Binding Id}" MinWidth="60" Width="SizeToCells"/>
</DataGrid.Columns>
</DataGrid>
</UserControl>
And I create additional columns in the code-behind, omitting boiler plate:
public partial class DataSetView : UserControl, IViewFor<DataSetViewModel>
{
// ViewModel DependencyProperty named "ViewModel" declared here
public DataSetView()
{
InitializeComponent();
this.WhenAnyValue(_ => _.ViewModel).BindTo(this, _ => _.DataContext);
this.OneWayBind(ViewModel, vm => vm.Users, v => v.UserDataGrid.ItemsSource);
this.Bind(ViewModel, vm => vm.SelectedUser, v => v.UserDataGrid.SelectedItem);
}
// this gets called when the ViewModel is set, and when I detect fields are added or removed
private void InitHeaders(bool firstInit)
{
// remove all columns except the first, which is reserved for Id
while (UserDataGrid.Columns.Count > 1)
{
UserDataGrid.Columns.RemoveAt(UserDataGrid.Columns.Count - 1);
}
if (ViewModel == null)
return;
// using all DataGridTextColumns for now
for (int i = 0; i < ViewModel.FieldHeaders.Count; i++)
{
DataGridColumn column;
switch (ViewModel.FieldHeaders[i].Type)
{
case DataSet.UserData.Field.FieldType.Text:
column = new DataGridTextColumn
{
Binding = new Binding($"Fields[{i}].Value")
};
break;
case DataSet.UserData.Field.FieldType.Image:
column = new DataGridTextColumn
{
Binding = new Binding($"Fields[{i}].Value")
};
break;
}
column.Header = ViewModel.FieldHeaders[i].Key;
column.Width = firstInit ? DataGridLength.SizeToCells : DataGridLength.SizeToHeader;
UserDataGrid.Columns.Add(column);
}
}
When Fields get added or remove, the UserViewModels are updated in DataSetViewModel and InitHeaders is called to recreate the columns. The resulting DataGridCells bind to their respective FieldViewModels and everything works.
What I'm Trying To Do (but doesn't work)
I would like to break FieldViewModel into two derived classes, TextFieldViewModel and ImageFieldViewModel. Each has their respective TextFieldView and ImageFieldView with their own ViewModel dependency property. UserViewModel still contains a ReactiveList. My new InitHeaders() looks like this:
private void InitHeaders(bool firstInit)
{
// remove all columns except the first, which is reserved for Id
while (UserDataGrid.Columns.Count > 1)
{
UserDataGrid.Columns.RemoveAt(UserDataGrid.Columns.Count - 1);
}
if (ViewModel == null)
return;
for (int i = 0; i < ViewModel.FieldHeaders.Count; i++)
{
DataGridTemplateColumn column = new DataGridTemplateColumn();
DataTemplate dataTemplate = new DataTemplate();
switch (ViewModel.FieldHeaders[i].Type)
{
case DataSet.UserData.Field.FieldType.Text:
{
FrameworkElementFactory factory = new FrameworkElementFactory(typeof(TextFieldView));
factory.SetBinding(TextFieldView.ViewModelProperty,
new Binding($"Fields[{i}]"));
dataTemplate.VisualTree = factory;
dataTemplate.DataType = typeof(TextFieldViewModel);
}
break;
case DataSet.UserData.Field.FieldType.Image:
{
FrameworkElementFactory factory = new FrameworkElementFactory(typeof(ImageFieldView));
factory.SetBinding(ImageFieldView.ViewModelProperty,
new Binding($"Fields[{i}]"));
dataTemplate.VisualTree = factory;
dataTemplate.DataType = typeof(ImageFieldViewModel);
}
break;
}
column.Header = ViewModel.FieldHeaders[i].Key;
column.Width = firstInit ? DataGridLength.SizeToCells : DataGridLength.SizeToHeader;
column.CellTemplate = dataTemplate;
UserDataGrid.Columns.Add(column);
}
}
The idea is that I create a DataGridTemplateColumn that generates the correct View and then binds the indexed FieldViewModel to the ViewModel dependency property. I have also tried adding a Converter to the Bindings that converts from the base VM to the correct derived type.
The end result is that the DataGrid populates with the correct view, but the DataContext is always a UserViewModel rather than the appropriate FieldViewModel-derived type. The ViewModel is never set, and the VMs don't bind properly. I'm not sure what else I'm missing, and would appreciate any suggestions or insight.
I've figured out an answer that works, though it may not be the best one. Rather than binding to the ViewModel property in my views, I instead bind directly to the DataContext:
factory.SetBinding(DataContextProperty, new Binding($"Fields[{i}]"));
In my views, I add some boilerplate code to listen to the DataContext, set the ViewModel property, and perform my ReactiveUI binding:
public TextFieldView()
{
InitializeComponent();
this.WhenAnyValue(_ => _.DataContext)
.Where(context => context != null)
.Subscribe(context =>
{
// other binding occurs as a result of setting the ViewModel
ViewModel = context as TextFieldViewModel;
});
}
The problems is simple: when ItemsSource is updated Combobox doesn't "refresh" e.g. new items don't appear to be added to the list of items in the combobox.
I've tried the solution from aceepted answer to this question: WPF - Auto refresh combobox content with no luck.
here's my code,
XAML:
<ComboBox Name="LeadTypeComboBox" ItemsSource="{Binding LeadTypeCollection}" />
ViewModel:
public ObservableCollection<XmlNode> LeadTypeCollection { get; set; }
the way I update this collection is in the separate method, which loads data from updated XML file: this.LeadTypeCollection = GetLeadTypesDataSource();
I've also tried using Add for testing purposes:
this.LeadTypeCollection = GetLeadTypesDataSource();
ItemToAdd = LeadTypeCollection[LeadTypeCollection.Count - 1];
this.LeadTypeCollection.Add(ItemToAdd);
the code updating collection definitely kicks off, I can see new items in this collection when debugging, but I don't see them in the combobox.
Doing this in the xaml code-behind works: LeadTypeComboBox.ItemsSource = MyViewModel.GetLeadTypesDataSource(); but I'd like to achieve this with MVVM, i.e. the code must be in ViewModel which isn't aware of LeadTypeComboBox control.
Firedragons answer would work, but i would prefer to initialize the LeadTypeCollection just once and use clear, add remove to update your collection.
var update = GetLeadTypesDataSource();
this.LeadTypeCollection.Clear();
foreach(var item in update)
{
this.LeadTypeCollection.Add(item);
}
your xaml binding should work if the datacontext is right
<ComboBox Name="LeadTypeComboBox" ItemsSource="{Binding LeadTypeCollection}" />
I think I have seen this before and the solution was to update the collection property to raise the change.
i.e.
public class MyViewModel : INotifyPropertyChanged
{
private ObservableCollection<XmlNode> leadTypeCollection;
public string LeadTypeCollection
{
get { return leadTypeCollection; }
set
{
if (value != leadTypeCollection)
{
leadTypeCollection = value;
NotifyPropertyChanged("LeadTypeCollection");
}
}
public MyViewModel()
{
leadTypeCollection = new ObservableCollection<XmlNode>();
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
PropertyChanged.Raise(this, info);
}
}
I have an extension method for raising the property (as found elsewhere on stackoverflow):
public static void Raise(this PropertyChangedEventHandler handler, object sender, string propertyName)
{
if (null != handler)
{
handler(sender, new PropertyChangedEventArgs(propertyName));
}
}
A simple method is to change ItemsSource with empty list and then change it back to your updated source. A snippet from my project which is working:
RulesTable.ItemsSource = Rules.rulesEmpty;
RulesTable.ItemsSource = Rules.Get();
I have a simple class that provides state codes like this:
public class StateProvider
{
static string[] _codes = new string[]
{
"AL",
"AK",
...
};
public string[] GetAll()
{
return _codes;
}
}
My model class that supports the view looks a little like this:
public class ProjectModel : ChangeNotifier
{
StateProvider _states = new StateProvider();
public ProjectModel()
{
Project = LoadProject();
}
ProjectEntity _project;
public ProjectEntity Project
{
get { return _project; }
set
{
_project = value;
FirePropertyChanged("Project");
}
}
public string[] States { get { return _states.GetAll(); } }
}
And my ComboBox XAML looks like this:
<ComboBox SelectedValue="{Binding Project.State, Mode=TwoWay}" SelectedValuePath="{Binding RelativeSource={RelativeSource Self}}" ItemsSource="{Binding States}" />
The binding works from the UI to the entity - if I select the state from the combo then the value gets pushed to the project entity and I can save it. However, if I shutdown and reload, the state code value doesn't bind from the entity to the UI and the combo shows nothing selected. Then, of course, a subsequent save nulls the entity's state value.
I want this very simple since I want to display state codes and save state codes (I don't want to display the full state name). So I don't want to have to muck with creating a State class that has Code and FullName properties and avoid having to use the SelectedValuePath and DisplayMemberPath properties of the combobox.
Edit:
Added to the code how ProjectModel does change notification. Note that the ProjectEntity class does this too. Trust me, it works. I've left it out because it also inherits from an Entity base class that does change notification through reflection. TwoWay binding works on everything but for the combobox.
You have to at least implement IPropertyNotifyChanged on your ProjectModel class
public class ProjectModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
and implement the Project property as below for binding to work other than 1-way-1-time.
public ProjectEntity Project
{
get { return (ProjectEntity)GetValue(ProjectProperty); }
set { SetValue(ProjectProperty, value); }
}
// Using a DependencyProperty as the backing store for Project.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty ProjectProperty =
DependencyProperty.Register("Project",
typeof(ProjectEntity),
typeof(ProjectModel),
new PropertyMetadata(null,
new PropertyChangedCallback(OnProjectChanged)));
static void OnProjectChanged(object sender, DependencyPropertyChangedEventArgs args)
{
// If you need to handle changes
}
Wow, whodathought it'd come down to this:
<ComboBox ItemsSource="{Binding States}" SelectedValue="{Binding Project.State, Mode=TwoWay}" />
It turned out it was the order in which I placed the attributes in the XAML. The SelectedValue binding was happening before the ItemsSource binding and thus there were no items in the combobox when the SelectedValue was bound.
Wow, this just seems like a really bad thing.
Change your ProjectModel class to this:
public class ProjectModel : ChangeNotifier
{
StateProvider _states = new StateProvider();
public ProjectModel()
{
Project = LoadProject();
States = new ObservableCollection<string>(_states.GetAll());
}
ProjectEntity _project;
public ProjectEntity Project
{
get { return _project; }
set
{
_project = value;
FirePropertyChanged("Project");
}
}
public ObservableCollection<string> States { get; set; }
}
Also make sure that ProjectEntity also implements INotifyPropertyChanged.
Problem: The data I'm trying to display is essentially a collection of collections of some object. So the rows can be any number, normal for a datagrid, and the columns are also any number. That's not so normal for a datagrid. Usually you have a set number of columns and your rows vary. The datagrid cell will be either a string or a value changeable via a combobox.
Attempted Solution: I tried adding columns to the datagrid dynamically, and while this worked just fine (adding them in codebehind) the real issue I hit was how to bind to the underlaying objects. The data is being built dynamically and I tried a couple of formats. I tried a collection of Arrays, and also an ObservableCollection of ObservableCollections. I could bind to the objects but as Bindings in Silverlight have to bind to properties I couldn't come up with a solution presenting the data this way.
My solution as a result has been to display the data in a more traditional manner, with a list and a datagrid. When you select an item in the list it repopulates the data in the datagrid to show the objects.
Question: is there a way to bind a datagrid cell to a collection of collections of objects?
I found this question (WPF) which looks similar and it didn't help any. I think it's the same issue.
WPF DataGrid: DataGridComboxBox ItemsSource Binding to a Collection of Collections
One possible solution is instead of using a simple collection as the inner object, create a class derived from a collection and implement ICustomTypeDescriptor on it. In the interface implementation, iterate over the elements of the collection and populate your property descriptor collection accordingly. Once you do that, you should be able to bind to those properties from XAML.
Example - a data object based on a dictionary, which you can bind against its key names (I compressed to a single line all trivial method implementations):
class DictionaryDataObject : Dictionary<string, object>, ICustomTypeDescriptor
{
#region ICustomTypeDescriptor Members
public AttributeCollection GetAttributes() { return AttributeCollection.Empty; }
public string GetClassName() { return "DictionaryDataObject"; }
public string GetComponentName() { return null; }
public TypeConverter GetConverter() { return null; }
public EventDescriptor GetDefaultEvent() { return null; }
public PropertyDescriptor GetDefaultProperty() { return null; }
public object GetEditor(Type editorBaseType) { return null; }
public EventDescriptorCollection GetEvents(Attribute[] attributes) { return EventDescriptorCollection.Empty; }
public EventDescriptorCollection GetEvents() { return EventDescriptorCollection.Empty; }
public PropertyDescriptorCollection GetProperties() { return GetProperties(null); }
public object GetPropertyOwner(PropertyDescriptor pd) { return this; }
public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
{
var pds =
this.Keys
.Select(x => new DictionaryPropertyDescriptor(x))
.ToArray();
return new PropertyDescriptorCollection(pds);
}
#endregion
}
class DictionaryPropertyDescriptor : PropertyDescriptor
{
public DictionaryPropertyDescriptor(string name) : base(name, null) { }
public override bool CanResetValue(object component) { return false; }
public override Type ComponentType { get { return null; } }
public override bool IsReadOnly { get { return false; } }
public override Type PropertyType { get { return typeof(object); } }
public override void ResetValue(object component) { }
public override bool ShouldSerializeValue(object component) { return false; }
public override object GetValue(object component)
{
var dic = component as DictionaryDataObject;
if (dic == null) return null;
return dic[Name];
}
public override void SetValue(object component, object value)
{
var dic = component as DictionaryDataObject;
if (dic == null) return;
dic[Name] = value;
}
}
Sample object setup from code behind:
DictionaryDataObject ddo = new DictionaryDataObject();
public Window4()
{
ddo["propa"] = 1;
ddo["propb"] = "foo";
ddo["propc"] = "bar";
ddo["propd"] = 4.5;
InitializeComponent();
DataContext = ddo;
}
XAML usage:
<Window.Resources>
<DataTemplate x:Key="template">
<WrapPanel>
<TextBlock Text="{Binding propa}" Margin="5"/>
<TextBlock Text="{Binding propb}" Margin="5"/>
<TextBlock Text="{Binding propc}" Margin="5"/>
<TextBlock Text="{Binding propd}" Margin="5"/>
</WrapPanel>
</DataTemplate>
</Window.Resources>
<Grid>
<ContentControl Content="{Binding}" ContentTemplate="{StaticResource template}"/>
</Grid>
If you want to adapt this solution to a list instead of a dictionary, you must set each property name based on some property of the list element.
I think I understand what you are trying to accomplish and I actually have a more elegant solution to your problem and it does not involve writing any custom classes. I wrote a blog post on this issue. The blog is geared towards the DataGrid from the Silverlight Toolkit, but you can easily modify it to use any grid.
The solution is here.
Let me know if this is what you were looking for.
I have a listbox defined in XAML as:
<ListBox x:Name="directoryList"
MinHeight="100"
Grid.Row="0"
ItemsSource="{Binding Path=SelectedDirectories}"/>
The SelectedDirectories is a property on the lists DataContext of type List<DirectoryInfo>
The class which is the datacontext for the listbox implements INotifyPropertyChanged. When the collection changes the items are added successfully to the list however the display does not update until I force the listbox to redraw by resizing it.
Any ideas why?
EDIT: INotifyPropertyChanged implementation
public class FileScannerPresenter : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private FileScanner _FileScanner;
public FileScannerPresenter()
{
this._FileScanner = new FileScanner();
}
public List<DirectoryInfo> SelectedDirectories
{
get
{
return _FileScanner.Directories;
}
}
public void AddDirectory(string path)
{
this._FileScanner.AddDirectory(path);
OnPropertyChanged("SelectedDirectories");
}
public void OnPropertyChanged(string property)
{
if (this.PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
Try
ObservableCollection<DirectoryInfo>
instead - you're triggering a refresh of the entire ListBox for no reason, and you don't need to make your hosting class implement INotifyPropertyChanged - it could easily just be a property of the window. The key is to never set the property to a new instance. So:
class SomeWindow : Window {
public ObservableCollection<DirectoryInfo> SelectedDirectories {get; private set;}
SomeWindow() { SelectedDirectories = new ObservableCollection<DirectoryInfo>(); }
public void AddDirectory(string path) {
SelectedDirectories.Add(new DirectoryInfo(path));
}
}
If you end up using that FileScanner class, you need to implement INotifyCollectionChanged instead - that way, the ListBox knows what to add/remove dynamically.
(See Update below). WPF seems to be working alright. I put your code into a new project. The listbox updates whenever I click the button to invoke AddDirectory. You should not need any more code changes.
The problem seems to be something else.. Are there multiple threads in your UI?
I didnt have the FileScanner type. So I created a dummy as follows.
public class FileScanner
{
string _path;
public FileScanner()
{ _path = #"c:\"; }
public List<DirectoryInfo> Directories
{
get
{
return Directory.GetDirectories(_path).Select(path => new DirectoryInfo(path)).ToList();
}
}
internal void AddDirectory(string path)
{ _path = path; }
}
No changes to your FileScannerPresenter class. Or your listbox XAML. I created a Window with a DockPanel containing your listbox, a textbox and a button.
Update: Paul Betts is right. It works because I return a new list each time from the Bound property. Data binding with lists always messes me up.
With more tinkering, the easy way to do this is:
Make FileScanner#Directories return an ObservableCollection<DirectoryInfo> (which implements INotifyCollectionChanged for you). Change all signatures all the way up to return this type instead of a List<DirectoryInfo>
FileScanner and FileScannerPresenter themselves do not have to implement any INotifyXXX interface.
// in FileScanner class def
public ObservableCollection<DirectoryInfo> Directories
{
get
{ return _DirList; }
}
internal void AddDirectory(string path)
{
_path = path;
//var newItems = Directory.GetDirectories(_path).Select(thePath => new DirectoryInfo(thePath)).ToList();
//_DirList.Concat( newItems ); -- doesn't work for some reason.
foreach (var info in Directory.GetDirectories(_path).Select(thePath => new DirectoryInfo(thePath)).ToList())
{
_DirList.Add(info);
}
}