I'm building an MVVM Light WPF app in Visual Studio 2015 with Entity Framework 6 (EF) providing the data. I have a ComboBox that displays the reasons why someone needs to take a drug test and it looks like this:
<ComboBox ItemsSource="{Binding ReasonsForTest}"
SelectedItem="{Binding Path=ReasonsForTestVm,
UpdateSourceTrigger=PropertyChanged}"
DisplayMemberPath="Description" />
The ReasonsForTest is of type ReasonForTestViewModel class:
public class ReasonForTestViewModel: ViewModelBase
{
private int _ReasonForTestId;
private string _ReasonForTestAbbr;
private string _description;
public int ReasonForTestId
{
get { return _ReasonForTestId; }
set
{
if (value == _ReasonForTestId) return;
_ReasonForTestId = value;
RaisePropertyChanged();
}
}
public string ReasonForTestAbbr
{
get { return _ReasonForTestAbbr; }
set
{
if (value == _ReasonForTestAbbr) return;
_ReasonForTestAbbr = value;
RaisePropertyChanged();
}
}
public string Description
{
get { return _description; }
set
{
if (value == _description) return;
_description = value;
RaisePropertyChanged();
}
}
}
I have a data service class that contains the following code to fetch the data for the valid values of the ComboBox:
public async Task<ObservableCollection<ReasonForTestViewModel>> GetReasonsForTest()
{
using (var context = new MyEntities())
{
var query = new ObservableCollection<ReasonForTestViewModel>
(from rt in context.ReasonForTests
orderby rt.description
select new ReasonForTestViewModel
{
ReasonForTestId = rt.ReasonForTestID,
ReasonForTestAbbr = rt.ReasonForTestAbbr,
Description = rt.description,
});
return await Task.Run(() => query);
}
}
The view model populates the ComboBox using this:
var dataService = new TestDataService();
ReasonsForTest = await dataService.GetReasonsForTest();
The ComboBox has the correct data; however, it's not selecting the correct value when the app starts -- it's showing blank on load. The SelectedItem (ReasonsForTestVm) is also of that class type ReasonForTestViewModel and gets populated from the database with the one item for this person. I've stepped through the code to ensure ReasonsForTestVm has the correct data, and it does.
Here's the property for ReasonsForTestVm:
public ReasonForTestViewModel ReasonForTestVm
{
get
{
return _reasonForTestVm;
}
set
{
if (Equals(value, _reasonForTestVm)) return;
_reasonForTestVm = value;
RaisePropertyChanged();
}
}
What am I doing wrong here? I'm about to lose my mind!
Update: Sorry for the confusing name in the property above. Fixed.
Any WPF items control that extends Selector (such as ComboBox and ListBox) has two properties that are often used in conjunction: ItemsSource and SelectedItem.
When you bind a collection to ItemsSource, a representation of those items are shown in the UI. Each one of the representations is bound to an instance found within the collection bound to ItemsSource. If, for an example, you're using a DataTemplate to create that representation, you'll find within each that the DataContext will be one of those instances from the collection.
When you select one of these representations, the SelectedItemproperty now holds the instance from the collection that was bound to that representation.
This works perfectly through user interaction with the UI. However, there's one important caveat when interacting with these controls programmatically.
It's a very common pattern to bind these properties to similar properties in your view model.
public class MuhViewModel
{
public MuhItems[] MuhItems {get;} = new[]{ new Item(1), new Item(2) };
// I don't want to show INPC impls in my sample code, kthx
[SuperSlickImplementINotifyPropertyChangedAttribute]
public MuhSelectedItem {get;set;}
}
bound to
<ComboBox ItemsSource="{Binding MuhItems}"
SelectedItem="{Binding MuhSelectedItem}" />
If you try to manually update the selected item this way...
muhViewModel.MuhSelectedItem = new Item(2);
The UI will not change. The Selector sees that ItemsSource has changed, yes, but it doesn't find that instance in the ItemsSource collection. It doesn't know that one instance of Item with a value of 2 is equivalent to any other Item with the same value. So it does nothing. (That's a bit simplistic for what really happens. You can bust out JustDecompile and see for yourself. It gets real convoluted down in there.)
What you should be doing in this situation is updating SelectedItem with an instance found within the collection bound to ItemsSource. In our example,
var derp = muhViewModel.MuhItems.FirstOrDefault(x => x.MuhValue == 2);
muhViewModel.MuhSelectedItem = derp;
Side note, when tracking instances within a debug session, it helps to use Visual Studio's Make Object ID feature.
Related
After a lot of hours I finally found what is the problem that cause the bug. Before to show the code that present the problem I need to explain the situation.
Binding and properties structure
In my application there is a ComboBox that bind as ItemSource a list of Rounds and as SelectedItem the Round selected from the list by the user.
The ComboBox have this structure:
<ComboBox ItemsSource="{Binding Rounds}" DisplayMemberPath="RoundName" SelectedItem="{Binding SelectedRound, Mode=TwoWay}" />
as you can see I've as modality TwoWay this allow me to update the property SelectedRound automatically when the user change the Item selected.
This is the class Round:
public class Round
{
public int Id { get; set; }
public string Link { get; set; }
public bool Selected { get; set; }
public string RoundName { get; set; }
}
and this is the properties used by the ComboBox:
//List of rounds available
private List<Round> _rounds;
public List<Round> Rounds
{
get { return _rounds; }
set
{
_rounds = value;
OnPropertyChanged();
}
}
//Selected round on ComboBox
private Round _selectedRound;
public Round SelectedRound
{
get { return _selectedRound; }
set
{
_selectedRound = value;
OnPropertyChanged();
}
}
both properties implement the OnPropertyChanged().
How the properties valorization works
In the app there is a method called LoadRounds() that is called each time the user press a button, this method have the following instruction:
public void LoadRounds(Team team)
{
//Fill the source of ComboBox with the rounds of the new team
Rounds = team.Rounds.ToList(); //<- Create a copy, so no reference
//Get the selected round
SelectedRound = Rounds?.FirstOrDefault(x => x.Id == team.CurrentRound.Id);
}
the SelectedRound is taken from a team property called CurrentRound, in particular each team have a round, so for a practice example:
[Rounds id available in Rounds property]
37487
38406
38405
37488
37486
...
[CurrentRound id of team]
38405
so the SelectedRound will contain the Round with Id 38405, and the linq query working well.
The problem
I set a breakpoint on _selectedRound = value;, the first firing time the value is a Round item (38405), but there is also a second firing time (that shouldn't be) that have as value null.
After a lot of hours spended on pc to understand why this situation happen I figure out.
Seems that the ComboBox (the TwoWay mode) doesn't know how to map the SelectedRound from the ItemSource, so essentially:
1. [Item Source updated with new Rounds]
2. [SelectedRound updated from the new `Rounds` available]
3. [SelectedRound setter called again with a null value]
I used also the stack call window for see if there is any method that call the setter property another time, but there is no external method that call the setter, so I guess is the TwoWay mode that fire the setter again.
How can I fix this situation? I know that this post is a bit complicated, I'm available to answer to all questions, and for provided more details if needed.
Thanks to all, have a good day.
UPDATE #1
This is my INotifyPropertyChanged implementation:
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
UPDATE #2
The method LoadRounds is called when the user change the selection on a DataGrid, the DataGrid contains all teams, so I get the team selected by the user on the DataGrid, and then call the method LoadRounds.
All the teams are contained in a DataGrid, the ItemSource is a List<Team>.
At the end of the method LoadRounds I save the current Round of the Team on a property called SelectedRoundSaved, simply doing:
SelectedRoundSaved = Clone(SelectedRound);
in this way I prevent to reload the Rounds if the SelectedRoundSaved is equal to SelectedRound.
the Clone method allow me to clone the object, and have this structure:
public T Clone<T>(T source)
{
if (ReferenceEquals(source, null))
{
return default(T);
}
var deserializeSettings = new JsonSerializerSettings { ObjectCreationHandling = ObjectCreationHandling.Replace };
return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(source), deserializeSettings);
}
it use the NewtonSoft.Json library.
This information isn't necessary at all, but as I said I'll add all info asked from you, thanks for the attention.
Are you sure this order is correct?
1. [Item Source updated with new Rounds]
2. [SelectedRound updated from the new `Rounds` available]
3. [SelectedRound setter called again with a null value]
After the combo box is initially bound I would expect the order to be (swapped the order of #2 and #3)
1. [Item Source updated with new Rounds]
2. [SelectedRound setter called again with a null value]
3. [SelectedRound updated from the new `Rounds` available]
This behavior follows what I would expect of a combo box.
When you update the ItemSource the ComboBox dumps its items and reloads with the new collection. Because the ComboBox is a Selector, it must then check its SelectedItem. If its SelectedItem is not found in the new collection it updates its SelectedItem to be null. All of this happens just because of the OnPropertyChanged(); call in the Rounds setter.
(Note: you will only see this behavior after a combo box has been loaded and bound)
Now there are many ways you can go about handling this, but IMO the simplest is merely to change the order of operations:
public void LoadRounds(Team team)
{
//Fill the source of ComboBox with the rounds of the new team
var newRounds = team.Rounds.ToList(); //<- Create a copy, so no reference
//Get the selected round
SelectedRound = newRounds.FirstOrDefault(x => x.Id == team.CurrentRound.Id);
Rounds = newRounds;
}
I have a method that queries a database using entity framework and places the results in an ICollectionView. The ICollectionView acts as the ItemsSource for a DataGrid. Everything works fine on the first query, but upon querying a second time, the data is not properly sorted, despite the application of the correct SortDescriptions.
Here is my code for trying querying and grouping/sorting the data:
CollectionViewSource cvsRS;
private ObservableCollection<productorder> rs;
public ObservableCollection<productorder> RS
{
get { return rs; }
set
{
if (rs != value)
{
rs = value;
OnPropertyChanged("RS");
}
}
}
private ICollectionView rsView;
public ICollectionView RSView
{
get { return rsView; }
set
{
if (rsView != value)
{
rsView = value;
OnPropertyChanged("RSView");
}
}
}
public void QueryDatabase()
{
RS = new ObservableCollection<productorder>(DatabaseEntities.productorders.Where(o => o.month.id == CurrentMonth.id));
if (RS != null)
{
cvsRS.Source = RS;
RSView = cvsRS.View;
RSView.GroupDescriptions.Clear();
RSView.GroupDescriptions.Add(new PropertyGroupDescription("producttype.productcategory.name"));
RSView.GroupDescriptions.Add(new PropertyGroupDescription("producttype.name"));
RSView.SortDescriptions.Clear();
RSView.SortDescriptions.Add(new SortDescription("producttype.productcategory.sortorder", ListSortDirection.Ascending));
RSView.SortDescriptions.Add(new SortDescription("client.name", ListSortDirection.Ascending));
RSView.Refresh();
CurrentRecord = null;
SelectedRecords = null;
}
}
The grouping works fine, but the groups aren't in the correct order based on the sorting. I've tried a number of possible "fixes" with no success (e.g. adding sort/group descriptions directly to the CollectionViewSource, sorting before grouping, removing some of the sorting/grouping, removing the SortDescriptions per CollectionViewSource does not re-sort on property change).
Does anyone know how to maintain the sort order regardless of how many queries are performed? I'm open to alternative methods of querying displaying the data in the DataGrid if that may work.
Try binding your CollectionViewSource.Source property to your ObservableCollection<T> property. Set up the binding in the viewmodel constructor. Then, just leave it alone. Update the ObservableCollection<T>, replace it, etc. As long as it's an ObservableCollection<T> and its public property raises PropertyChanged whenever you replace it, the whole thing will work.
public MyViewModel()
{
BindCollectionViewSource();
}
protected void BindCollectionViewSource()
{
cvsRS = new CollectionViewSource();
var binding = new Binding
{
Source = this,
Path = new PropertyPath("RS")
};
BindingOperations.SetBinding(cvsRS, CollectionViewSource.SourceProperty, binding);
}
// Since we're not going to be messing with cvsRS or cvsRS.View after the
// constructor finishes, RSView can just be a plain getter. The value it returns
// will never change.
public ICollectionView RSView
{
get { return cvsRS.View; }
}
You can't just assign a binding to Source; there's more to it than that. The Source="{Binding RSView}" stuff you see in XAML may look like an assignment, but some details are being hidden for convenience. The Binding actively does stuff. It needs to know who the target object is.
I did see one funny thing: I gave my test code one PropertyGroupDescription and one SortDescription. When I added items to the collection, it sorted them within the groups. Then when I called RSView.Refresh(), it resorted them without reference to the groups. Not sure I understood what it was doing there.
I've started an MVVM project and now I'm stucking with correct DataBinding.
My project has:
A UserControl whit a ViewModel as DataContext like:
public partial class TestUserControl: UserControl
{
public TestUserControl()
{
this.DataContext = new TestUserControlViewModel();
}
}
ViewModel code is (BaseViewModel class contains PropertyChangedEventHandler):
public class TestUserControlViewModel : BaseViewModel
{
public KrankenkasseControlViewModel()
{}
public IEnumerable<DataItem> GetAllData
{
get
{
IGetTheData src= new DataRepository();
return src.GetData();
}
}
}
IGetTheData is the interface to DataContext:
public interface IGetTheData
{
IEnumerable<DataItem> GetData();
}
}
and finally the DataRepository code:
public class DataRepository : IGetTheData
{
private TestProjectDataContext dax = new TestProjectDataContext();
public IEnumerable<DataItem> GetData()
{
return (from d in this.dax.TestData
select new DataItem
{
ID = d.ID,
SomeOtherData = d.SomeOtherData
});
}
}
My UserControl has a few TextBoxes, but what's the best way to bind correctly?
Thanks for your help, regards.
EDIT: Binding the data against multiple textboxes
After reading your comment, I will elaborate my example for textboxes.
First important thing is that the ViewModel will model the things in the View, so that the View gets all information it needs in the structure it needs. That means, if you have multiple textboses in the View, you will need multiple string Properties in your ViewModel, one for each textbox.
In your XAML you could have something like
<TextBox Text="{Binding ID, Mode=TwoWay}" />
<TextBox Text="{Binding SomeOtherData, Mode=TwoWay}" />
and in your ViewModel
public class TestUserControlViewModel : BaseViewModel {
private string id;
private string someOtherData;
public TestUserControlViewModel() {
DataItem firstItem = new DataRepository().GetData().First();
this.ID = firstItem.ID;
this.SomeOtherData = firstItem.SomeOtherData;
}
public string ID {
get {
return this.id;
}
set {
if (this.id == value) return;
this.id = value;
this.OnPropertyChangedEvent("ID");
}
}
public string SomeOtherData {
get {
return this.someOtherData;
}
set {
if (this.someOtherData == value) return;
this.someOtherData = value;
this.OnPropertyChangedEvent("SomeOtherData");
}
}
}
Here I assume that in your BaseViewModel there is an OnPropertyChangedEvent method to fire the corresponding event. This tells the View that the property has changed and it must update itself.
Note the Mode=TwoWay in the XAML. This means, that it doesn't matter on which side the value changes, the other side will reflect the change immediately. So if the user changes a value in a TwoWay bound TextBox, then the corresponding ViewModel property will automatically change! And also vice versa: if you change the ViewModel property programmatically, the View will refresh.
If you want to show multiple textboxes for more than one data item, then you must introduce more Properties in the ViewModel and bind them accordingly. Maybe a ListBox with a flexible number of TextBoxes inside is a solution then, like #Haspemulator already answered.
Binding the data against a collection control
In the TestUserControl I guess you have a control (like a ListView) to show the list of loaded things. So bind that control against the list in the ViewModel with
<ListView ... ItemsSource="{Binding GetAllData}" ... />
First you must understand that Binding means not "read the data and then forget the ViewModel". Instead you bind the View to the ViewModel (and its Properties) as long as the View lasts. From this point of view, AllData is a much better name than GetAllData (thanks #Malcolm O'Hare).
Now in your code, every time the View reads the AllData property, a new DataRepository is created. Because of the Binding, that is not what you want, instead you want to have one instance of DataRepository for the whole lifetime of the View, which is used to read the initial data and can later be used to update the View, if the underlying database changes (maybe with an event).
To enable such a behavior you should change the type of the AllData property to an ObservableCollection, so that the View can automatically update the list if changes occur.
public class TestUserControlViewModel : BaseViewModel
private ObservableCollection<DataItem> allData;
public TestUserControlViewModel() {
IGetTheData src = new DataRepository();
this.allData = new ObservableCollection<DataItem>(src.GetData());
}
public ObservableCollection<DataItem> AllData {
get {
return this.allData;
}
}
public void AddDataItem(DataItem item) {
this.allData.Add(item);
}
}
Now if you call AddDataItem later, the ListView will update itself automatically.
Your Property Name is bad. You should call it AllData, not GetAllData.
Since you are returning a collection, you probably should be using some sort of list control (ListBox, ListView).
In that case you'd be doing
<ListBox ItemsSource="{Binding GetAllData}" />
Guten Abend. :) As it already mentioned, since you're returning the collection, it's better to use a ListBox. The comment about having ObservableCollection as a cache is also absolutely valid. I would add that if you need to have your data editable, you should use TextBox inside the ItemTemplate:
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Text={Binding SomeOtherData,Mode=TwoWay} />
</DataTemplate>
</ListBox.ItemTemplate>
In this case if user edits the text in the box, data will be updated in your data object, so that it could be saved in the database later.
I have a list of items with id and description(i can introduce key-value collection instead if needed). What i need is control that binded to viewmodel id property, but shows description of corresponding item/pair on it. Closest example i know is combobox, where i set DisplayMemberPath and SelectedValue/SelectedValuePath, but i don't need dropdown. So is there any in-built control in Silverlight for this?
(of course i can code one myself, it's easy and I can even just put some logic for viewmodel to get pair i need and bind it's description to simple textblock)
Edit: To illustrate what funcionality i need i coded simple example class. It actually satisfies my needs, but i still want to know if i can use built-in control.
public class CollectionItemDisplayControl:TextBox
{
public CollectionItemDisplayControl()
{
IsReadOnly = true;
}
public string SelectedID
{
get { return (string)GetValue(SelectedIDProperty); }
set { SetValue(SelectedIDProperty, value); }
}
// Using a DependencyProperty as the backing store for SelectedID. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectedIDProperty =
DependencyProperty.Register("SelectedID", typeof(string), typeof(CollectionItemDisplayControl), new PropertyMetadata(new PropertyChangedCallback(OnSelectedIDChangedStatic)));
private static void OnSelectedIDChangedStatic(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
CollectionItemDisplayControl originator = d as CollectionItemDisplayControl;
if (originator != null)
{
originator.OnSelectedIDChanged(e);
}
}
private void OnSelectedIDChanged(DependencyPropertyChangedEventArgs e)
{
string description = String.Empty;
string value = e.NewValue as string;
if (value != null)
{
foreach (var item in _items)
{
if (item.UniqueID == value)
{
description = item.Description;
break;
}
}
}
Text = description;
}
private IDataCollection _viewModel;
public IDataCollection ViewModel
{
get { return _viewModel; }
set
{
_viewModel = value;
if (_viewModel != null)
{
_items = _viewModel.Items;
}
}
}
private ObservableCollection<IUnique> _items = new ObservableCollection<IUnique>();
}
ItemClass contains two properties: ID and Description. I can place this control on the page, bind Items, and one-way bind SelectedID.
Edit 2: well i didn't make SelectedID DependencyProperty so binding won't work, but i will fix it right away
Edit 3: first snippet was sloppy and didn't work properly, so i fixed it.
If I understood properly,
You just need the right binding implemented.
(you do need a list? not just a single item, even if single it's similar just any control)
Bind the list to e.g. ItemsControl.
Set ItemsSource to your list of items
Then override ToString on your Item providing it's 'yours' really. If not you can make your own wrapper.
Within ToString output whatever is presenting your item, e.g. description.
That's a quickest solution, you can also make item template as you want.
EDIT:
well just put everything in the view model and bind to it - the TextBox, i.e.
Text={Binding SelectedText}
e.g.
...in your view model add SelectedText and SelectedID (and Items if needed) - properly do OnPropertyChanged.
Set SelectedID from view model or if 'bound' from another control that may change it.
Within set for SelectedID set the SelectedText.
No need for a control for things like that, it's all data binding really.
About Data Virtualizatoin in WPF, the WPF: Data Virtualization is a good article.
With using this, Data Virtualization was executed as good in my code but there is the one problem, which is that I cannot bind a property in ViewModel with SelectedItem of ItemsControl in View. If one item of data satisfies some condition while data loads, the one item will be set as a property in ViewModel and then it will be bound with SelectedItem of ItemsControl in View, but will not.
My code about this is the following. About the types of IItemsProvider andVirtualizingCollection, please refer to the WPF: Data Virtualization.
So far, I have tried:
I'm sure that if Data Virtualization were not used, the Selected Item Binding would be cool.
The IndexOf(T item) method in VirtualizingCollection returns always -1. As thinking this would be the problem, I implemented that the IndexOf(T item) returns a actual index, but it was not concerned with this problem.
The code of implementing IItemsProvider
public class WordViewModelProvider : IItemsProvider<WordViewModel>
{
private string _searchText = "some text";
public WordViewModel SelectedItem
{
get;
private set;
}
#region IItemsProvider<WordViewModel> Members
public int FetchCount()
{
lock (_words)
{
int count = (from word in _words
where word.Name.Contains(_searchText)
select word).Count();
return count;
}
}
public IList<WordViewModel> FetchRange(int startIndex, int count)
{
lock (_words)
{
//Please, regard _word as IEnumerable<Word>
IQueryable<Word> query = (from word in _words
where word.Name.Contains(_searchText)
select word);
List<WordViewModel> result = query.ToList().ConvertAll(w =>
{
var wordViewModel = new WordViewModel(w, _searchText);
if (w.Name.Equals(_searchText, StringComparison.InvariantCultureIgnoreCase))
{
SelectedItem = wordViewModel;
}
return wordViewModel;
});
return result;
}
}
#endregion
}
The code of using VirtualizingCollection in ViewModel
public void ViewList()
{
var wordViewModelProvider = new WordViewModelProvider();
var virtualizingCollection = new VirtualizingCollection<WordViewModel>(wordViewModelProvider);
//IList<WordViewModel> type to bind with View's ItemsSource.
WordViewModels = virtualizingCollection;
//WordViewModel type to bind with View's SelectedItem
SelectedItem = wordViewModelProvider.SelectedItem;
}
I would like to post good references about Virtualization to deal with large data set in WPF.
UI Virtualization vs Data Virtualization.
For Virtualization approaches:
Paul McClean
Vincent Van Den Berghe
bea.stollnitz: He/She describes the solution that combines some of the best features of the two former and covers my issue.