I have problem with ordering data for ListView. I have EventDisplay class which is an ObservableCollection for ListView(called Events)
private ObservableCollection<EventDisplay> currentEvents = new ObservableCollection<EventDisplay>();
private void Events_Loaded(object sender, RoutedEventArgs e)
{
sv = (ScrollViewer)VisualTreeHelper.GetChild(VisualTreeHelper.GetChild(this.Events, 0), 0);
Events.ItemsSource = currentEvents;
}
I then add new data by function :
private void LoadDataToList(List<EventDisplay> newItems)
{
foreach (EventDisplay ed in newItems)
{
//Set some additional data
currentEvents.Add(ed);
}
//When this line below is commented ListView data is updated
//but is not sorted, when i uncomment the listview data is not being updated
//currentEvents = new ObservableCollection<EventDisplay>(currentEvents.OrderByDescending(x => x.ed.date).ToList());
}
So what is the proper way of ordering data for ListView in Windows 8.1 apps ?
You can sort & filter the view of your ObservableCollection (explanation here)
public class ViewableCollection<T> : ObservableCollection<T>
{
private ListCollectionView _View;
public ListCollectionView View
{
get
{
if (_View == null)
{
_View = new ListCollectionView(this);
}
return _View;
}
}
}
Data structure for the example:
interface ICustomer
{
string CuctomerName{get;set;}
int Age{get;set;}
}
Example use of the code:
ViewableCollection<ICustomer> vCustomers = new ViewableCollection<ICustomer>();
// Sorting settings:
ViewableCollection<ICustomer> vCustomers.View.SortDescriptions.Add(new SortDescription("CustomerName", ListSortDirection.Ascending));
vCustomers.View.Filter = MyCustomFilterMethod;
// add data to collection
MyCustomers.ToList().ForEach(customer => vCustomers.Add(customer));
Examlpe filter method:
private bool MyCustomFilterMethod(object item)
{
ICustomer customer = item as ICustomer;
return customer.Age > 25;
}
when you need to refresh the filter, the only thing you need to do is call:
this.vCustomers.View.Refresh();
Then you bind your GUI to vCustomers.View
You don't need to reset binding sources etc.
Use this for your add items code:
foreach (EventDisplay ed in newItems.OrderByDescending(x => x.ed.date).ToList()
{
//Set some additional data
currentEvents.Add(ed);
}
The reason your doesn't work is that you are reassigned the currentEvents reference rather than updating the ObservableCollection.
You should do the following :
currentEvents = new ObservableCollection<EventDisplay>(currentEvents.OrderByDescending(x => x.ed.date).ToList());
Events.ItemsSource = currentEvents;
This forces the ListView to rebind to your new sorted observable collection.
Another option is to sort the Observable collection in place. However, it may introduce flickering as the ListView will constantly update as the sort progresses.
If you don't want the ScrollView to reset its position, you can save the scrollview position and then restore it after sorting the list.
I've had success with Implementing a custom ObservableCollection that supports sorting but prevents UI flickering by suspending change notification during sort and then issuing a reset notification. The ScrollView should stay at its current position even when confronted with the reset event.
Related
I have a ViewModel which contains an ObservableCollection<CustomKeyGroup<CustomItem>> property bound to a control in a View and the problem is that I want to sort this collection by a property in CustomKeyGroup<T>, without setting the ObservableCollection<...> object property (i.e. sort the collection inline):
public class MainViewModel : ViewModelBase {
... // data service etc code
private ObservableCollection<CustomKeyGroup<CustomItem>> _items = new ObservableCollection<CustomKeyGroup<CustomItem>>();
public ObservableCollection<CustomKeyGroup<CustomItem>> Items
{
get
{
return _items;
}
set
{
_items = value;
RaisePropertyChanged("Items");
}
}
public void Sort(string _orderBy = null, bool _descending = true) {
if (string.IsNullOrEmpty(_orderBy) || this.Items.Count == 0) {
return;
}
var test = this.Items.ToList();
// bubble sort
try {
for (int i = test.Count - 1; i >= 0; i--) {
for (int j = 1; j <= i; j++) {
CustomKeyGroup<CustomItem> o1 = test[j - 1];
CustomKeyGroup<CustomItem> o2 = test[j];
bool move = false;
var order = typeof(CustomKeyGroup<CustomItem>).GetProperty(orderBy);
var t = order.GetValue(o1);
var t2 = order.GetValue(o2);
// sort comparisons depending on property
if (_descending) { // ascending
if (t.GetType() == typeof(int)) { // descending and int property
if ((int)t < (int)t2) {
move = true;
}
} else { // descending and string property
if (t.ToString().CompareTo(t2.ToString()) > 0) {
move = true;
}
}
} else { // ascending
if (t.GetType() == typeof(int)) { // ascending and int property
if ((int)t > (int)t2) {
move = true;
}
} else { // ascending and string property
if (t.ToString().CompareTo(t2.ToString()) < 0) {
move = true;
}
}
}
// swap elements
if (move) {
//this.Items.Move(j - 1, j); // "inline"
test[j] = o1;
test[j - 1] = o2;
}
}
}
// set property to raise property changed event
this.Items = new ObservableCollection<CustomKeyGroup<CustomItem>>(test);
} catch (Exception) {
Debug.WriteLine("Sorting error");
}
//RaisePropertyChanged("Items"); // "inline sort" raise property changed to update Data binding
Debug.WriteLine("Sorted complete");
}
... // get data from service, etc.
From the code above, the attempted inline sorts are commented out (as they do not update the control that databinds to it), and the manual setting of Items are left in (works, but if you scroll down the control and sort, it will take you back to the top - undesirable!).
Anyone have any idea how I can update the view/control using an inline sort option? I've also tried manually raising the RaisePropertyChanged event (specified in ObservableObject using the MVVMLight Toolkit) to no avail.
Note: Setting a breakpoint at the end of the try-catch reveals that the ObservableCollection<...> is indeed sorted, but the changes just do not reflect in the View! Even weirder is that the control (LongListSelector) has a JumpList bound to another property of CustomKeyGroup<T> and it successfully updates instantly!! If I tap on any of these items in the JumpList, the View correctly updates itself, revealing the sorted items... I then thought of setting the DataContext of the View after sorting, but that also does not solve the issue.
Thanks.
Adding my own answer here.
So following the comments from the original post, #piofusco points out that a View does not update when an ObservableCollection has only been sorted. Even manually changing the collection (hence, raising NotifyPropertyChanged or NotifyCollectionChanged) does not update it.
Searching around a little more, I decided I could make use of CollectionViewSource, which would do my sorting for me - without changing the collection itself (hence allowing the control to retain its current scroll position). To get it working, basically, add a new property to the ViewModel of type CollectionViewSource, add a SortDescription, set its Source and bind directly to that property (instead of the original ObservableCollection:
In ViewModel:
private CollectionViewSource _sortedCollection = new CollectionViewSource();
public CollectionViewSource SortedCollection {
get {
_sortedCollection.Source = this.Items; // Set source to our original ObservableCollection
return _sortedCollection;
}
set {
if (value != _sortedCollection) {
_sortedCollection = value;
RaiseNotifyPropertyChanged("SortedCollection"); // MVVMLight ObservableObject
}
}
}
View XAML (note the binding to Property.View):
<ListBox ItemsSource="{Binding SortedCollection.View}" ... />
And in your View code-behind, if you have a Sort button:
ViewModel _vm = this.DataContext as ViewModel;
viewModel.SortedCollection.SortDescriptions.Clear(); // Clear all
viewModel.SortedCollection.SortDescriptions.Add(new SortDescription("PropertyName", ListSortDirection.Descending)); // Sort descending by "PropertyName"
And boom! Your sorted collection should update instantly in the View! Even better is that it retains our ObservableCollection functionality in that any updates to objects in the ObservableCollection will raise the NotifyPropertyChanged or NotifyCollectionChanged handlers, thereby updating the View (allowing for both sorting and updating of objects while retaining current scroll positions)!
Note: For those out there using a LongListSelector control, I wasn't able to get it to work, and with a little more internet-digging with I came across this post, which, discusses why LLS cannot bind to a CollectionViewSource.View without some modifications. So I ended up using a ListBox control instead. You can read about some of the differences here. For my task though, the ListBox will suffice.
I have run over a small performance problem while I was trying to add some Items of an ObservableCollection to a ListView.
Here's the ObservableCollection:
private ObservableCollection<object> children = new ObservableCollection<object>();
public ObservableCollection<object> Children
{
get
{
return children;
}
}
I'm also using a ContentProperty:
[ContentProperty(Name="Children")]
Here's the CollectionChanged Event I need to start adding the List:
void children_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
InitGraphics();
}
In InitGraphics(), I will create my List and draw all elements needed for it. There's also a function called SynchronizeList(), which gets called right at the beginning of InitGraphics().
SynchronizeList() looks like this:
private void SynchronizeList()
{
foreach (var item in children)
{
//Casting ListViewItem as ValueBoxControl
ValueBoxControl control = (ValueBoxControl)item;
//Standards and needed attributes
control.ValueTextBox.IsHitTestVisible = false;
control.ClipColor = ClipColor;
control.Foreground = this.Foreground;
//Add the Item to the List
List.Items.Add(item);
}
}
Now, as you can see, my ListView is called List and Items are Usercontrols called ValueBoxControl. The standards and attributes have to be the same for every item and they should be unchangeable.
I need the SynchronizeList() function in a few other functions (for example: When the List gets toggled twice, I delete all Items and add them again with SynchronizeList(), etc.).
The problem occurs when I add about 50+ Items, because it obviously has to run through the foreach loop 50 times and add each item. While debugging, I have noticed that the children Collection doesn't have all 50 items from the start. It first has 1 item, then 2, then 3, etc.
Which means that the SynchronizeList() gets called 50 times, while first adding 1 item, then 2, then 3, etc.
I have tried many things (like working with e.NewItems in the CollectionChangedEvent), but I currently see no possible solution.
Here's a complete example out of my fragments which shows my problem:
//ContentProperty to receive Items from XAML with the help of an ObservableCollection
[ContentProperty(Name="Children")]
public sealed partial class ListViewControl : ValueBoxControl
{
//Create a new ListView
ListView List = new ListView();
//Initialization
public ListViewControl()
{
//Events
children.CollectionChanged += children_CollectionChanged;
}
//ObservableCollection to receive Items from XAML
private ObservableCollection<object> children = new ObservableCollection<object>();
public ObservableCollection<object> Children
{
get
{
return children;
}
}
//Event which gets called whenever the List in XAML gets changed
void children_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
//Calls the function to draw the List
InitGraphics();
}
private void InitGraphics()
{
SynchronizeList();
if (List.Items.Counter >= 1)
{
//Draw the ListView with a custom design
}
}
private void SynchronizeList()
{
//Get each item in the ObservableCollection and add some attributes
foreach (var item in children)
{
//ListViewItem casted as ValueBoxControl
ValueBoxControl control = (ValueBoxControl)item;
//Standards & Limitations
control.ValueTextBox.IsHitTestVisible = false;
control.ClipColor = ClipColor;
control.Foreground = this.Foreground;
//Add Item to ListView
List.Items.Add(item);
}
}
}
The main problem I have here is that the CollectionChanged Event gets called 50 times for 50 items, each time receiving a new item, instead of receiving them all in one go.
Given the documentation for ItemCollection, I suspect you want the ReplaceAll method:
For implementations that track a "changed" event, the initial reset fires an event, but the items added don't fire discrete per-item events.
So something like:
private void SynchronizeList()
{
// Set properties in each item
foreach (var item in children)
{
//ListViewItem casted as ValueBoxControl
ValueBoxControl control = (ValueBoxControl)item;
//Standards & Limitations
control.ValueTextBox.IsHitTestVisible = false;
control.ClipColor = ClipColor;
control.Foreground = this.Foreground;
}
List.Items.ReplaceAll(children.ToArray());
}
Unfortunately it doesn't look like there's an AddRange which is what you really want... it's at least worth giving that a try.
I can't seem to figure it out, because I don't seem to be able to cast my ListView's items to the ListViewItem type and call ListViewItem.Focus(). The following won't work because the ListView's items are of type LogRecord:
((ListViewItem)listView.Items[0]).Focus();
EDIT: I want the scrollbar to move to where the item is, basically, or better said, that the item becomes visible in the list of items the user sees.
Any ideas on how I can get my ListView to focus in on a particular item? Right now its bound to a collection. Here's how I set up my ListView object:
listView = new ListView();
Grid.SetRow(listView, 1);
grid.Children.Add(listView);
GridView myGridView = new GridView();
// Skipping some code here to set up the GridView columns and such.
listView.View = myGridView;
Binding myBinding = new Binding();
myBinding.Source = PaneDataContext.LogsWrapper;
listView.SetBinding(ItemsControl.ItemsSourceProperty, myBinding);
I bind to this data type (LogRecord contains things like LogRecord.Message that corresponds to a Message column on the grid view, etc.; and the code works):
public class LogRecordWrapper : IEnumerable<LogRecord>, INotifyCollectionChanged
{
public List<LogRecord> RowList { get; set; }
public event NotifyCollectionChangedEventHandler CollectionChanged;
public LogRecordWrapper()
{
RowList = new List<LogRecord>();
}
protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (CollectionChanged != null)
{
CollectionChanged(this, e);
}
}
public void SignalReset()
{
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset, null));
}
public void Add(LogRecord item)
{
RowList.Add(item);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item));
}
public IEnumerator<LogRecord> GetEnumerator()
{
return RowList.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
ListView.ScrollIntoView
ListBox.ScrollIntoView Method
The link says ListBox but it works with ListView also
As for not working with Focus please post how you are using ScrollIntoView.
Could you use:
listView.Items[0].Focused = true;
...or perhaps:
listVIew.Items[0].Selected = true;
(I'm not positive what kind of "focus" you are after)
Then combine with (or use in place):
listView.Items[0].EnsureVisible();
This is amazing!!!!
05-29-2002, 04:53 PM #1 Jim Guest
ListView EnsureVisible not working. Any ideas?
The code ... in fact find and highlight selected item, it’s just not scrolling
to it and making it visible. The user is forced to scroll to the item.
04-04-2004, 12:07 AM luchmun
Hi,
working on a form...Everything goes fine but the problem is that even i use the lstitem.ensurevisible with listitem.selected = true the current entry does not become visible.
11 YEARS later and still it does not work and no one, not even Microsoft seem to know why?
The answer that works for me is listview1.ensurevisible(itemindex) NOT listview.items(itemindex).ensurevisible
I'm still in the learning Phase of WPF, EF and MVVM and now I got the following problem. I can delete and insert new items in my DataGridView but I don't know how to update my items.
All I do is select an emptyrow which already has a primary key and then I put the data into it. It's working (updating database) but the GridView is not refreshing. I Need to restart the program first to see my updated data.
My Execute Command to Update my Database. I'm in the ViewModel class
public void ExecuteUpdate(object obj)
{
try
{
SelectedIndex.Child_Update(new Farbe { FarbauswahlNr = SelectedIndex.FarbauswahlNr, Kurztext = SelectedIndex.Kurztext, Ressource = SelectedIndex.Ressource, Vari1 = SelectedIndex.Vari1, Vari2 = SelectedIndex.Vari2 });
//ListeAktualisieren --> Refreshing the List
ListeAktualisieren();
}
catch (Exception e)
{
MessageBox.Show(e.ToString());
}
}
Here is my Refresh Method which SHOULD Refresh the GridView. I'm in the ViewModel class
public void ListeAktualisieren()
{
farbliste.ListeAktualisieren(db);
farbliste.Model = farbliste.Model.Concat(farbliste.Addlist).ToList();
Model = farbliste.Model;
farbliste.Addlist.Clear();
}
The method is calling my Business List which also got a Refresh Method. Reading from my database here. I'm in the Business List class
public void ListeAktualisieren(TestDBEntities db)
{
Model.Clear();
foreach (var item in db.Farben)
{
//Insert and delete working
add = new Farbe { FarbauswahlNr = item.FarbauswahlNr, Kurztext = item.Kurztext, Ressource = item.Ressource, Vari1 = Convert.ToBoolean(item.Var1), Vari2 = item.Vari2 };
Addlist.Add(add);
}
}
Model is the Source of my GridView which is not Refreshing changed data when Updated but is showing new data rows when inserting or deleting.
You need Observablecollections and Classes with implemented INotifyPropertyChanged. Add the new element to the Observablecollection by insert and raise the event propertychanged by a change.
The rest should be done by WPF.
Edit: The Sourcecollection for the DataGrid needs to be the Observablecollection.
Edit2: To be nice I put the result of the comments here ;-)
Each row of the DataGrid is an element of the collection. Each cell of one row listens to a PropertyChangedEvent of its element (the String is Casesensitive so be carefull). If the getter of the property isn't called after the propertychangedevent the binding didn't receive the event.
This piece of Code can help asure that you don't call with nonexistent strings:
private void VerifyPropertyName(string PropertyName)
{
if (string.IsNullOrEmpty(PropertyName))
return;
if (TypeDescriptor.GetProperties(this)(PropertyName) == null) {
string msg = "Ungültiger PropertyName: " + PropertyName;
if (this.ThrowOnInvalidPropertyName) {
throw new isgException(msg);
} else {
Debug.Fail(msg);
}
}
}
Try adding this to your binding section
ItemsSource="{Binding Path=Model, UpdateSourceTrigger= PropertyChanged"}
I'm using a PagedCollectionView to bind an ObservableCollection<T> to a DataGrid in my Silverlight app. In this case, the source collection can incur an unbounded number of updates during its lifespan. It seems, however, that if I'm using a PropertyGroupDescription to group the elements in the DataGrid then I need to reapply that grouping using PagedCollectionView.GroupDescriptions.Add(...) every time the source collection is updated with an element that doesn't fit into an existing grouping. Is there any way to make the groupings refresh/recalculate automatically?
public ObservableCollection<DataItem> DataItems
{
get { return _dataItems; }
private set { _dataItems = value; }
}
public PagedCollectionView ItemsView
{
get { return _itemsView; }
private set { _itemsView = value; }
}
public MyGridControl()
{
// Initialize data members
DataItems = new ObservableCollection<DataItem>();
ItemsView = new PagedCollectionView(GridDataItems);
Loaded += new RoutedEventHandler(MyGridControl_Loaded);
}
private void MyGridControl_Loaded(object sender, RoutedEventArgs e)
{
_myGrid.ItemsSource = ItemsView;
}
public void RegisterDataItem(DataItem item)
{
DataItems.Add(item);
/* To get grouping to work like I want, I have to add this:
* ItemsView.GroupDescriptions.Clear();
* ItemsView.GroupDescriptions.Add(new PropertyGroupDescription("GroupName"));
*/
}
public void UnregisterError(DataItem item)
{
DataItem.Remove(item);
}
Thanks, and let me know if I can provide any additional information.
So, as far as I can tell, the grouping operation appears to be a once-off operation that is performed on the data as it exists at the time that the PagedCollectionView.GroupDescriptions collection is manipulated. It seems that the desired groupings do indeed need to be re-applied when the source collection is changed.
I did find one alternative approach for my specific case. Since I'm using an ObservableCollection for the source collection of the view, I can wire up something to the CollectionChanged event of that source collection wherein the PagedCollectionView.GroupDescriptions collection gets cleared and the desired GroupDescription is then reapplied. This doesn't seem totally compliant with good OO practices, nor is it usable if the source collection for the view doesn't implement INotifyCollectionChanged.
I'll leave this open a little longer in case anyone can offer another approach, otherwise I'll just concede.
Thanks again.
Looking at the code in Reflector, the Groupings are correctly processed but if you use view in a DataGrid with IsReadOnly == true you won't see the groupings reflected while changing the source collection.