While working on an Universal App (currently only on the WP8.1-side), I've stumbled upon the following weird thing.
I've got a ComboBox, the UserControl (located in the WindowsPhone-project) it's in is binded to a VM in the Shared project. Both the ItemsSource and SelectedItem are binded to their respective properties in the VM.
When running the application, when you select any item except the first one, it is working perfectly. But, when I select the first item, the string displayed in the ComboBox shows the .ToString()-method of the VM instead...
(Btw, it's a simple List<string>, the selected item is a string. It can't get much more simpler than that :p)
I've created a sample app, containing just this Combobox, and the VM. I was able to reproduce this, the moment I asynchronously fill in property binded to the ItemsSource. When doing it from a synchronous method, it works. But just filling it from an async method provides the above problem.
A few screenshots:
The first one shows the app when it's loaded. When the collection changes, the first element of the list is selected. It is shown here:
When you click on the ComboBox, you get to see its items as usual:
Say you click on any element other than the first, you still get normal behaviour:
So far, so normal. Now click the first item. You get this:
...
I've tried a variety of things like making it a list of an object instead of just strings. Adding a converter to the binded objects, just for debugging purposes, only reveales the actual string-values. I've got no idea how, nor why, the binded SelectedItem suddenly shows the DataContext of the ComboBox...
You can download the sample app here:
http://1drv.ms/1DhklCQ
(contains no binaries, just the code)
Anybody got any ideas?
EDIT: The code required to reproduce this issue:
Create a blank Universal store app (8.1).
In the WindowsPhone project, the file MainPage.xaml:
I've added a simple combobox, and catch the Loaded event.
<ComboBox ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}" />
In its code behind. I've assigned the DataContext to the VM. And in the Loaded event I asychronously call the VM.LoadData()
private VM _vm = new VM();
public MainPage()
{
this.InitializeComponent();
this.DataContext = _vm;
}
private async void Page_Loaded(object sender, RoutedEventArgs e)
{
await _vm.LoadDataAsync();
}
The VM object is defined as followed:
public class VM : INotifyPropertyChanged
{
private List<string> _items;
public List<string> Items
{
get { return _items; }
set
{
_items = value;
_selectedItem = _items.FirstOrDefault();
RaisePropertyChanged("Items");
RaisePropertyChanged("SelectedItem");
}
}
private string _selectedItem;
public string SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
RaisePropertyChanged("SelectedItem");
}
}
public VM()
{
}
public async Task LoadDataAsync()
{
this.Items = new List<string>()
{
"a",
"b",
"c",
"d",
"e",
"f",
};
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
}
Found a workaround cause previous solutions didn't solve my problem.
Just add a pause between binding and selecting an item or index of your combobox.
Code below :
myCombobox.ItemsSource = myList;
await Task.Delay(100);
myCombobox.SelectedIndex = 12;
Hope this helps !
I checked it out and I can't see any problem with your code, so I guess it is a bug in the ComboBox.
Understanding the problem and finding a true fix may take you some time, so I'd suggest you use some workaround that works for you. I tried the following and it seemed to work:
Change the Items property in the VM to be of type ObservableCollection<string>
Initialize the property/field in the VM's constructor to an empty collection.
When loading the items, just fill the collection (add items to it using the Add() method) instead of replacing it.
Edit: example of how I fill tested it.
public class VM : INotifyPropertyChanged
{
private ObservableCollection<string> _items;
public ObservableCollection<string> Items
{
get { return _items; }
set
{
_items = value;
_selectedItem = _items.FirstOrDefault();
RaisePropertyChanged("Items");
RaisePropertyChanged("SelectedItem");
}
}
private string _selectedItem;
public string SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
RaisePropertyChanged("SelectedItem");
}
}
public VM()
{
this._items = new ObservableCollection<string>();
}
public async Task LoadDataAsync()
{
var items = new List<string>() {
"1",
"b",
"c",
"d",
"e",
"f",
"f",
"f",
"f",
"f",
"f",
};
foreach (var i in items) {
this._items.Add(i);
}
this.SelectedItem = items.FirstOrDefault();
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
}
This works fine for me.
Not only asynchronously - if you put _vm.Items = new List... in OnLoaded event, instead of await _vm.LoadDataAsync(); - you will get the same issue.
Seems that the issue won't occur once you set your Items before setting the DataContext.
The other thing is that the issue won't appear (as I've tried) if you don't set Selected item from code:
public ObservableCollection<string> Items
{
get { return _items; }
set
{
_items = value;
// _selectedItem = _items.FirstOrDefault();
RaisePropertyChanged("Items");
// RaisePropertyChanged("SelectedItem");
}
}
As for now I've no idea why this happens.
Related
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.
I have an ObservableCollection<string> named MyCollection containing "A", "B", "C", "D". I create a view like this:
<ComboBox x:Name="MyComboBox"
ItemsSource="{Binding MyCollection}"
SelectedIndex="{Binding 3}"/>
<Button Click="OnClickButton">Button</Button>
Then my codebehind looks like this:
public partial class MyClass {
private string _mySelection;
public string MySelection
{
get { return _mySelection; }
set
{
_mySelection = value;
}
}
public void OnClickButton(object sender, RoutedEventArgs e) {
MySelection = (MyComboBox.SelectedItem).ToString();
MessageBox.Show(MySelection);
}
This is fine. The ComboBox populates just as it should, and MySelection is set properly and appears in the message box. But currently, my ComboBox appears blank in the user interface until the user clicks on it and selects an option. I want option C to be the default value, and if the user doesn't select anything, then MySelection will be set to C.
But no matter how many different combinations of SelectedItem, SelectedValue, and SelectedIndex I try, I can't get it to work. The ComboBox always starts off empty.
How can I do this?
Set a default value of the _mySelection field, i.e. "C"
Or, more general, set the value of MySelection to the desired default value after construction.
Also make sure that the MySelection property fires a change notification.
public class MyViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<string> MyCollection { get; }
= new ObservableCollection<string>();
private string mySelection;
public string MySelection
{
get { return mySelection; }
set
{
mySelection = value;
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(MySelection)));
}
}
}
Initialize the DataContext of the view with an instance of MyViewModel:
public MainWindow()
{
InitializeComponent();
var vm = new MyViewModel();
vm.MyCollection.Add("A");
vm.MyCollection.Add("B");
vm.MyCollection.Add("C");
vm.MyCollection.Add("D");
vm.MySelection = "C";
DataContext = vm;
}
private void OnClickButton(object sender, RoutedEventArgs e)
{
MessageBox.Show(((MyViewModel)DataContext).MySelection);
}
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 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));
}
}