I have a problem. I created a ListView with as itemsource a List called unknownDeviceList from my ViewModel. Here is my ViewModel:
public class VM_AddDeviceList : BindableObject
{
private List<UnknownDevice> _unknownDeviceList;
public List<UnknownDevice> unknownDeviceList
{
get
{
return _unknownDeviceList;
}
set
{
if (_unknownDeviceList != value)
{
_unknownDeviceList = value;
OnPropertyChanged();
}
}
}
public List<UnknownDevice> deviceList_raw;
public VM_AddDeviceList()
{
deviceList_raw = new List<UnknownDevice>();
unknownDeviceList = new List<UnknownDevice>();
MyHandler();
}
private async Task LoadUnknownDeviceList()
{
deviceList_raw = await App.RestService.GetDevices();
foreach (UnknownDevice device in deviceList_raw)
{
bool containsItem = App.knownDeviceList.Any(item => item.MAC == device.MAC);
if (!containsItem)
{
unknownDeviceList.Add(device);
}
}
}
public Task MyHandler()
{
return LoadUnknownDeviceList();
}
}
Now I can see that unknownDeviceList gets filled in the foreach, but on the screen the ListView stays empty. What am I doing wrong?
Something with the async and await?
You are raising PropertyChanged when setting unknownDeviceList to inform the view that the list has changed. Anyway, there is no way for the view to know that there were items added to unknownDeviceList.
The most idiomatic way to solve the issue would be to use an ObservableCollection<string> instead.
private ObservableCollection<string> _unknownDevices = new ObservableCollection<string>();
public ObservableCollection<string> UnknownDevices => _unknownDevices;
Please note that I've used the expression body syntax for read-only properties for UnknownDevices, it's not a field.
Since ObservableCollection<string> implements INotifyCollectionChanged which can be subscribed to by the binding to UnknownDevices the view is informed about the changes in UnknownDevices and will be updated when any items are added or removed.
Related
This is my first post in this forum, though I am a long-time lurker. I have started learning WPF for about a couple of months now, and I am trying to create an application just for training purposes.
I have a backend database which I have added to my application using EF6 ORM. In my application, I have a `ComboBox which needs to be populated by a column in a table of the database. That I can do using binding to a list.
The part I am having trouble with is the DataGrid. The columns of the DataGrid needs to be populated according to the Item chosen in the ComboBox.
My database:
As you can see, the school has several departments, and each of those department has a HOD and a student strength.
My application:
The ComboBox will be populated with school names. The DataGrid will be populated once the schoolname is selected. The DataGrid will have each row for each department available for the school. So I need to bind the corresponding columns with the departments of the corresponding schools. That much I get. However, then I want to save the user-entered comments in the Feedback TextBox.
I cannot understand how to create a class so that I can bind the DataGrid to the object of it. Is it possible to bind the DataGrid to an object and then bind the columns separately to another object?
EDIT
Apart from the entities created from the database, I have two classes:
class Feedback : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _school;
public string School
{
get
{
return _school;
}
set
{
_school = value;
OnPropertyChanged("School");
}
}
private ObservableCollection<FeedbackLine> _feedbackLines;
public ObservableCollection<FeedbackLine> FeedbackLines
{
get
{
return _feedbackLines;
}
set
{
_feedbackLines = value;
OnPropertyChanged("FeedbackLines");
}
}
public Feedback(string school)
{
//Insert some Linq Query to populate the FeedbackLines
//something like
//var FeedbackLines = Context.Schools.Where(c => c.SchoolName == school)
// .Select(c => new {Department = c.AvailableDepts.Dept, etc etc}.ToList();
//but then what?
}
private void OnPropertyChanged(string v)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(v));
}
}
This is supposed to be bound to the datagrid. And the FeedbackLine is:
public class FeedbackLine: INotifyPropertyChanged
{
private string _dept;
public string Department
{
get { return _dept; }
set { _dept = value;
OnPropertyChanged("Department");
}
}
private string _HOD;
public string HOD
{
get { return _HOD; }
set { _HOD = value;
OnPropertyChanged("HOD");
}
}
private int _strength;
public int Strength
{
get { return _strength; }
set { _strength = value;
OnPropertyChanged("Strength");
}
}
private bool _isSelected;
public bool Selected
{
get { return _isSelected; }
set { _isSelected = value;
OnPropertyChanged("Selected");
}
}
private string _comment;
public string Comment
{
get { return _comment; }
set { _comment = value;
OnPropertyChanged("Comment");
}
}
private void OnPropertyChanged(string v)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(v));
}
public event PropertyChangedEventHandler PropertyChanged;
}
I haven't had much headway with the ViewModel. Problem is, I am not very good with LINQ. And there are too many classes and objects and I have no idea which one to bind with which. The only vague idea that I can get is that I have to use LINQ to query the database using the selected School, and then populate the FeedbackLines using that.
Edit 2:
For anyone who's interested, here's my model diagram in WPF:
Model Diagram
Edit 3:
I think I am confused about ViewModel. The data that will be displayed on the screen is not necessarily the data to be saved. For example, I don't want to save the unselected rows. My Feedback class tries to display the data as well as save it. And therein lies the problem. Can't a DataGrid be bound to an object, while its columns be bound to other objects? For example, if I choose to use a Combobox for Department. Then I need to use ItemsSource for displaying items, but need to save the SelectedItem only. I can't find a way to separate these two concerns.
I would change your Feedback constructor
public Feedback(string school, List<FeedbackLine> feedbackLines)
{
School = school;
FeedbackLines = new ObservableColleytion<FeedbackLine>(feedbackLines);
}
It's a better architecture if your data viewmodel does not have a connection to the database. You can put your select in a seperate class.
If you need help with your LINQ statement I can help you.
In your Feedback constructor you wrote
//but then what?
When you got your data you can create instances of FeedbackLines to add them in the new constructor I showed above.
When you did this your viewmodel (which is DataContext of your view) needs an
public void ObservableCollection<Feedback> Feedbacks
with INotifyPropertyChanged like you did it in the other viewmodels.
In your xaml you have your ComboBox with the schools. Give that combobox a name, e.g. SchoolsComboBox.
In your DataGrid write this line
Source={Binding ElementName=SchoolsComboBox, Path=SelectedItem.FeedbackLines}
/edit for adding LINQ
You created an anonymous type. Just create a FeedbackLine instead and you're fine.
var feedbackLines = Context.Schools.Where(c => c.SchoolName == school)
.Select(c => new FeedbackLine
{
Department = c.AvailableDepts.Dept,
HOD = c.AvailableDepts.HeadOfDept,
Strength = c.AvailableDepts.StudentStrength}
.ToList()
U can make Something like this. Im sure it can be writen better but it works.
In your ViewModel make 3 Properties that implements INotifyPropertyChanged
One for your collection that will you bind to your ComboBox(make it ObservableCollection), One for SelectedItem from your ComboBox( you bind it to SelectedItem in comboBox) and another ObservableCollection that you will Bind to DataGrid)
For example you have in XAML:
<Grid>
<ComboBox ItemsSource="{Binding Products}"
SelectedItem="{Binding SelectedProduct}"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Width="200"
Margin="20"
IsSynchronizedWithCurrentItem="True" />
<DataGrid ItemsSource="{Binding SelectedOne}"
HorizontalAlignment="Right "
VerticalAlignment="Center"
Width="300"
IsSynchronizedWithCurrentItem="True">
</DataGrid>
and in your ViewModel you can have something like this.
public ObservableCollection<Product> Products
{
get { return _products; }
set
{
if (value != _products)
{
_products = value;
OnPropertyChanged();
}
}
}
private ObservableCollection<Product> _selectedOne;
public ObservableCollection<Product> SelectedOne
{
get { return _selectedOne; }
set {
_selectedOne = value;
OnPropertyChanged();
}
}
public int SelectedProductId
{
get { return _selectedProductId; }
set
{
if (value != _selectedProductId)
{
_selectedProductId = value;
OnPropertyChanged();
}
}
}
public Product SelectedProduct
{
get { return _selectedProduct; }
set
{
if (value ! = _selectedProduct)
{
_selectedProduct = value;
// clear your list of selected objects and then add just selected one
// or you dont clear it, and items will be added in DataGrid when selected in ComboBox
SelectedOne.Clear();
SelectedOne.Add(_selectedProduct);
OnPropertyChanged();
}
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged = delegate { };
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
Code to Populate Products, DataGrid will be populated by selecting Item from ComboBox.
u can go in ViewModel constructor and make something like this.
public MainWindowViewModel()
{
if (!DesignerProperties.GetIsInDesignMode(new DependencyObject()))
{
using (YourDbContext context = new YourDbContext ())
{
var productList = new ObservableCollection<Product>(context.Products);
productList.ToList()
Products = productsList;
}
}
}
I create a simple Treeview that I bound to an ObservableCollection.
ObservableCollection<IMarketDataViewModel> MarketDataItems;
public interface IMarketDataViewModel
{
string Title { get; }
ObservableCollection<IMarketDataViewModel> Items { get; set; }
}
public MarketDataUserControl(IMarketDataViewer viewModel)
{
InitializeComponent();
DataContext = viewModel;
marketDataTreeView.ItemsSource = viewModel.MarketDataItems;
}
When I update data in my ViewModel, I only see the first level in my Treeview. The only way I found to resolve the problem is to create an event in my ViewModel and when the data is updated instead calling PropertyChange on MarketDataItems, I trigger the event and the View reset marketDataTreeView.ItemsSource like this :
private void ViewModelOnOnUpdateItems()
{
marketDataTreeView.ItemsSource = null;
marketDataTreeView.ItemsSource = viewModel.MarketDataItems;
}
And this work perfectly --> All levels are displayed.
Someone know why the PropertyChange doesn't work and why I have to reset the ItemsSource ?
I think you should implement a binding to the ItemSource and this is done by a property:
// Create property
public ObservableCollection<IMarketDataViewModel> MarketDataItems { get; private set; }
...
// Create Binding
Binding bindingObject = new Binding("MarketDataItems");
bindingObject.Source = this; //codebehind class instance which has MarketDataItems
marketDataTreeView.SetBinding(TreeView.ItemsSource, bindingObject);
Or the binding in XAML:
<TreeView x:Name="marketDataTreeView" ItemsSource="{Binding Path=MarketDataItems}"/>
Finally the issue is that I didn't call OnPropertyChanged("Items")
public class MarketDataViewModelBase : IMarketDataViewModel, INotifyPropertyChanged
{
.....
private ObservableCollection<IMarketDataViewModel> items;
public ObservableCollection<IMarketDataViewModel> Items
{
get { return items; }
set
{
items = value;
OnPropertyChanged("Items"); //Add this line fix my issue
}
}
}
I stumbled on this problem a few days ago and nothing seems to give me a solution - or, at least, an idea. In my ViewModel A I have a calculated property that produces an ObservableCollection of ViewModels B. I can bind to this collection no problem, but changes in the properties of ViewModel B items don't show up in the UI. Any ideas will be greatly appreciated and don't hesitate to ask for any clarification or detail. Thanks in advance!
Update: Here's the property that doesn't notify
public Boolean IsHighlighted
{
get { return _IsHighlighted; }
set
{
if (_IsHighlighted != value)
{
_IsHighlighted = value;
OnPropertyChanged("IsHighlighted");
}
}
}
and the calculated property that produces the collection in ViewModel A
public ObservableCollection<PointViewModel> MidPoints
{
get
{
ObservableCollection<PointViewModel> midPoints = new ObservableCollection<PointViewModel>();
//
//....calculations
//
return midPoints;
}
}
Do not create a new ObservableCollection<PointViewModel> instance in the MidPoints property getter.
Instead, perform add and delete operations on the existing instance:
private readonly ObservableCollection<PointViewModel> midPoints
= new ObservableCollection<PointViewModel>();
public ObservableCollection<PointViewModel> MidPoints
{
get { return midPoints; }
}
public void UpdateMidPoints()
{
// performs calculations that add and remove elements to/from midPoints
// ...
}
In case it is for whatever reason required to create a new collection instance, you would have to raise the PropertyChanged event:
private ObservableCollection<PointViewModel> midPoints;
public ObservableCollection<PointViewModel> MidPoints
{
get { return midPoints; }
set
{
midPoints = value;
OnPropertyChanged("MidPoints");
}
}
public void UpdateMidPoints()
{
ObservableCollection<PointViewModel> newMidPoints
= new ObservableCollection<PointViewModel>();
//
// calculations...
//
MidPoints = newMidPoints;
}
I have a ListView bound to a view model which contains an ObservableCollection in a simple example app.
public class ViewModel : INotifyPropertyChanged
{
public ViewModel(){}
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<Item> _items;
public ObservableCollection<Item> Items
{
get
{
return this._items;
}
set
{
if (value != this._items)
{
this._items = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Items"));
}
}
}
}
public class Item
{
public string Name;
}
The following function is bound to the SelectionChanged event of the ListView
private void ItemListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
model.Items.Add(new Item { Name = "added item" });
model.Items = new ObservableCollection<Item> { new Item { Name = "new item 1" }};
}
When the event fires, this should happen
new item ("added item") appended to existing ObservableCollection
ObservableCollection set to a new collection [single item, "new item 1"]
What actually happens:
ObservableCollection set to new collection [single item, "new item 1"]
new item ("added item") appended to end of new collection
Can anyone explain why these are happening in the wrong order?
Can anyone explain why these are happening in the wrong order?
My guess is that they're not happening in the wrong (reversed) order but the append is happening twice. Executing
model.Items = ... ;
in the SelectionChanged of those same Items is pretty bold. It will trigger a SelectionChanged again, and only because the Selection then remains at none (Index -1) you do not get into an infinite loop.
observable collection not need inotify, try this:
public class ViewModel : INotifyPropertyChanged
{
public ViewModel(){}
public ObservableCollection<Item> Items ;
}
A ObservableCollection
private ObservableCollection<string> _items = new ObservableCollection<string>();
public ObservableCollection<string> Items { get { return _items; } }
is updated on user interactions (a TextBox event).
In a ListBox I'll show the current values
Binding listBinding = new Binding {Source = Items};
listbox.SetBinding(ListBox.ItemsSourceProperty, listBinding);
That works so far: When adding a new value the list is immediately updated.
But now I have to requirements:
Sort values
Add one item at the beginning of the list
I solved as followed:
public IEnumerable<string> ItemsExtended
{
get
{
return new[] { "first value" }.Concat(Items.OrderBy(x => x));
}
}
I changed the binding to that IEnumerable and the list contains a sorted list with "first value" at position one.
Unfortunately when the list should be updated on user interaction it does not work any more. Even changing IEnumerable to ObservableCollection again and directly referring to the private ObservableCollection does not solve the issue:
return new ObservableCollection<string> (new[] { "bla" }.Concat(_items.OrderBy(x => x)));
How to update the list when _items changes?
Off the top of my head. You could implement INotifyPropertyChanged for the class that your collection belongs to . After that add a CollectionChanged handler for your _items collection and fire PropertyChanged("ItemsExtended") in that handler. Also using yield return in the getter will avoid creating a new collection just to add the item at the top.
It should look something like this
public partial class MyClass : INotifyPropertyChanged
{
ObservableCollection<string> _items;
public MyClass()
{
_items = new ObservableCollection<string>();
_items.CollectionChanged += (s, e) => { OnPropertyChanged("Items"); };
}
public IEnumerable<string> Items
{
get
{
yield return "first value";
foreach (var item in _items.OrderBy(x=>x))
yield return item;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string property)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}