ICollectionView Sort doesn't work after changing source - c#

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.

Related

ObservableCollection not returning the new data after it is set

When populating an observable collection, I can see that the "return" is not being called when I "set" the new data in the collection. It does work if I set the data from a different location in the program so I must be not understanding some nuance of the way it works. The part that works is when I take out the commented code under "This works", "ChooseFile()" does not. In the debugger I can see the OptionsToChoose has data in both cases. When it works the XAML is updated correctly.
class ScripterViewModel : BindableBase
{
public ScripterViewModel()
{
ScripterModel scripterModel = new ScripterModel();
ObservableCollection<string> tabsChoice = new ObservableCollection<string>();
tabsChoice.Add("Tabs");
tabsChoice.Add("Buttons");
Tabs = tabsChoice;
this.OpenFileBtn = new DelegateCommand(chooseFile, canChooseFile).ObservesProperty(() => OpenFile);
this.SaveFileBtn = new DelegateCommand(saveFile, canSaveFile).ObservesProperty(() => SaveFile);
//This works
//var myJSONDoc = JsonConvert.DeserializeObject<JSONclass>(File.ReadAllText(#"C:\Users\mike\Documents\Haas\Scripter\settings.json"));
//OptionsToChoose = new ObservableCollection<Tabbed>(myJSONDoc.TabbedBtns);
}
public void chooseFile()
{
var myJSONDoc = JsonConvert.DeserializeObject<JSONclass>(File.ReadAllText(#"C:\Users\mike\Documents\Haas\Scripter\settings.json"));
OptionsToChoose = new ObservableCollection<Tabbed>(myJSONDoc.TabbedBtns);
}
public ObservableCollection<Tabbed> _optionsToChoose = new ObservableCollection<Tabbed>();
public ObservableCollection<Tabbed> OptionsToChoose
{
get
{
return _optionsToChoose;
}
set
{
_optionsToChoose = value;
}
}
}
When you are creating the OptionsToChoose in the constructor it will be initialized when the viewmodel is used by the view.
In the example that is not working, you are just replacing the ObservableCollection with a new one instead clearing it and adding the items. Therefore you need to notify that the property has been changed like V.Leon pointed out in his answer.
Or just clear the existing collection and populate it with the values from the json.
var myJSONDoc = JsonConvert.DeserializeObject<JSONclass>(File.ReadAllText(#"C:\Users\mike\Documents\Haas\Scripter\settings.json"));
OptionsToChoose.Clear();
foreach (var item in myJSONDoc.TabbedBtns)
{
OptionsToChoose.Add(item);
}
You are not raising PropertyChanged event in the setter of OptionsToChoose. You already extend BindableBase, so raising PropertyChanged event can be done by replacing your current OptionsToChoose property implementation with the following:
public ObservableCollection<Tabbed> OptionsToChoose
{
get
{
return _optionsToChoose;
}
set
{
SetProperty(ref _optionsToChoose, value);
}
}
See BindableBase.SetProperty Method
Ideally, you should not change the whole reference of ObservableCollection after it is binded. Instead clear items in it and then add new items in it.
public ObservableCollection<Tabbed> _optionsToChoose = new ObservableCollection<Tabbed>();
public ObservableCollection<Tabbed> OptionsToChoose
{
get
{
return _optionsToChoose;
}
}
OptionsToChoose.Clear();
OptionsToChoose.Add(foo);
As has already been brought up, given your code you would need to make the property for your collection raise PropertyChanged if you were resetting the collection. That said ObservableCollection is really not an ideal collection type to use. What I would recommend is including MvvmHelpers in your project and using the ObservableRangeCollection
public class MyPageViewModel : BindableBase
{
public MyPageViewModel()
{
OptionsToChoose = new ObservableRangeCollection<Tabbed>();
SomeCommand = new DelegateCommand(OnSomeCommandExecuted);
}
public DelegateCommand SomeCommand { get; }
public ObservableRangeCollection<Tabbed> OptionsToChoose { get; }
private void OnSomeCommandExecuted()
{
// get some updated data
IEnumerable<Tabbed> foo = DoFoo();
OptionsToChoose.ReplaceRange(foo);
}
}
You get a couple of benefits there. One you're not allocating and deallocating your collection. Also the ObservableRangeCollection updates the full list before raising PropertyChanged or CollectionChanged events this results in few UI notifications and better app performance.

Set SelectedItem of ComboBox from object

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.

ComboBox not able to set DataContext

I am using ComboBox in XAML:
<ComboBox x:Name="Combobox1" ItemsSource="{Binding}" Margin="0,0,300,0"
Width="100" FontSize="30" />
in code behind, I am setting its value to:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
Combobox1.DataContext = ComponentDataSource.ComponentCollection;
}
Now I have a data source:
public class ComponentDataSource
{
private static ObservableCollection<ComponentGroup> _componentcollection;
public static ObservableCollection<ComponentGroup> ComponentCollection
{
get { return _componentcollection; }
}
public static async void CheckJson(object sender, object e)
{
var client = new HttpClient();
client.MaxResponseContentBufferSize = 1024 * 1024;
try
{
var response = await client.GetAsync(new Uri("URI"));
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadAsStringAsync();
var jobj = JObject.Parse(result);
var list = jobj.Children()
.Cast<JProperty>()
.Select(p => new ComponentGroup()
{
Name = p.Name,
Type = (string)p.Value["P1"],
Value = (string)p.Value["P2"]
})
.ToList();
_componentcollection = new ObservableCollection<ComponentGroup>(list);
}
catch (HttpRequestException ex)
{
}
}
}
For some reason, these items are not showing up in the ComboBox.
All I get is an empty ComboBox.
Can anybody please help me out?
Edit 1: Hi, I know I am missing something simple but if anybody can please help me out I will highly appreciate it. BTW, if you want code let me know and I will upload it to skydrive.
Vasile has it right in the comments. XAML in Windows 8 Apps can not bind to static properties. XAML can bind to standard properties like this:
public string Name { get; set; }
That (above) will bind with the equiv. of the {Binding Mode=OneTime}. That's because updates to raise events. At the same time you can use fully evented properties like this:
string m_Name = default(string);
public string Name { get { return m_Name; } set { SetProperty(ref m_Name, value); } }
That will bind in XAML, supporting whatever mode you specify (OneTime, OneWay, TwoWay). It follows the INotifyPropertyChanged pattern and is pretty basic.
I say all that to say this. Those two approaches are the only way to bind. You cannot bind to a field. You cannot bind to a method (yet). And you cannot bind to a static property. If you must bind to a static property, just expose your static property in a standard property in your view model.
To show you I am telling the truth, consider this XAML (almost like yours):
<ComboBox x:Name="MyCombo" ItemsSource="{Binding}" />
If you try this, it will bind fine:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
this.MyCombo.DataContext = new MyModel().Items;
base.OnNavigatedTo(e);
}
public class MyModel
{
public MyModel()
{
foreach (var item in Enumerable.Range(1, 50))
Items.Add(item);
}
ObservableCollection<int> m_Items = new ObservableCollection<int>();
public ObservableCollection<int> Items { get { return m_Items; } }
}
The only difference between the two is the static part. So even pushing the values the way you are trying will not work. It's because you are binding directly to a static property.
Here's a way to have static and expose it so it will bind:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
this.MyCombo.DataContext = new MyModel().Items;
base.OnNavigatedTo(e);
}
public class MyModel
{
public MyModel()
{
foreach (var item in Enumerable.Range(1, 50))
s_Items.Add(item);
}
private static ObservableCollection<int> s_Items = new ObservableCollection<int>();
public ObservableCollection<int> Items { get { return s_Items; } }
}
In the code above, I have the value static in the MyModel class, but I have a standard property exposing it. Because it is an ObservableColelction the property is already evented for binding, so this could be the complete implementation. So you get static and binding. Make sense?
Now to the embarrassing part.
The technique you are using to bind in your question should work. Binding a static property to the DataContext and then binding to it in ItemsSource works. #maad0 has demonstrated that it's a fine approach. So, why isn't it working for you.
I suppose all I can say is "It's not because you are binding wrong". <blush /> I was tempted to delete my answer, but I am leaving it only because the explanation of static binding might be valuable to developers who are attempting to do so.
Test your code with this
This is your CheckJson method. I have added a debugger statement to see if you are actually getting any results. Try using this sample in your app and seeing if it breaks. If it does not break, then the problem is not that you do not have data. The problem is somehow in your binding. Though it all looks fine to me.
public static async void CheckJson(object sender, object e)
{
var client = new HttpClient();
client.MaxResponseContentBufferSize = 1024 * 1024;
try
{
var response = await client.GetAsync(new Uri("URI"));
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadAsStringAsync();
var jobj = JObject.Parse(result);
var list = jobj.Children()
.Cast<JProperty>()
.Select(p => new ComponentGroup()
{
Name = p.Name,
Type = (string)p.Value["P1"],
Value = (string)p.Value["P2"]
})
.ToList();
// add this code
if (!_componentcollection.Any())
System.Diagnostics.Debugger.Break();
_componentcollection = new ObservableCollection<ComponentGroup>(list);
}
catch (HttpRequestException ex)
{
}
}
There are a few things in your code that might result in an empty ComboBox:
Is your web service actually returning results? That exception that you are swallowing might hide the fact that you are actually working with an empty (or null) list. This is not your problem right now, but it might be.
Assuming your collection has elements and since you are using code behind, why don't you simply assign the ItemsSource property?
protected override void OnNavigatedTo(NavigationEventArgs e)
{
Combobox1.ItemsSource = ComponentDataSource.ComponentCollection;
}
This will show your items if your collection is not empty.
Despite what others suggest, your binding is correct, since you are not binding to a static property. You are binding to an object, your DataContext and you set this to be the value of a static property. If your collection is not null and has values before you assign it to your combo box's DataContext, then the items will show.
What I assume is happening in your case, is that you are setting the DataContext either to a value of null or, in the best case, to an empty collection.
Your CheckJson method looks very much like the event handler for a DispatchTimer, so I am assuming that you are regularly downloading elements from the web to display them in your combo box. However, notice that each time you do so, you are replacing the collection that contains them with a new ObservableCollection!
To test my two assumptions, all you have to do is call CheckJson (and make sure it runs completely) before you assign the DataContext.
The easiest way to fix your problem, especially since your collection is static, is to store an ObservableCollection once in _componentcollection (for example, in the class's static constructor or by initializing it when it's declared) and then simply Add, Remove, or Clear items as necessary. Since your combo box will always listen to the same list, it will be notified when its contents change. You'd have:
private static ObservableCollection<ComponentGroup> _componentcollection = new ObservableCollection<ComponentGroup>();
And in CheckJson, you replace:
_componentcollection = new ObservableCollection<ComponentGroup>(list);
with:
_componentcollection.Clear();
foreach (var item in list)
{
_componentcollection.Add(item);
}
PS. My suggestion, above, of using ItemsSource directly, will suffer from the exact same problem as the current code, it just takes the binding out of the picture and proves that the binding is not the problem. You would still have to be careful not to replace the list in your static code, for example, by using a single ObservableCollection as I suggest towards the end of my answer.

Is there such built-in control in silverlight?

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.

Using Data Virtualization, the problem of binding a property in ViewModel with SelectedItem of ItemsControl in View

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.

Categories

Resources