Wpf application and Threads - c#

I have problem with my GUI and Threads.
The GUI contains DataGrid. Every X time the program do some query and getting a list of items that I want to fill into the DataGrid.
So far so good:
private void loadTaskList() //Call every X time
{
List<myObject> myList = myquery();
this.Dispatcher.Invoke((Action)(() =>
{
TaskListTable.Items.Clear(); //Clear the DataGrid
foreach (myObject O in myList) //Add the items from the new query.
{
TaskListTable.Items.Add(O);
}
}));
FindSelectionObject(); // <-- see next explanation.
}
When the user click on one of the objects in the datagrid, the line color changed (it works fine), but when the program reload the table,The painted line disappears (Becuse I clear and add new objects).
To deal with it, I created the function FindSelectionObject():
private void FindSelectionObject()
{
this.Dispatcher.Invoke((Action)(() =>
{
this.SelectedIndex = TaskListTable.Items.IndexOf((myObject)lastSelectionObject); //find index of the new object that equels to the last selection object.
var row = TaskListTable.ItemContainerGenerator.ContainerFromIndex(SelectedIndex) as DataGridRow; //get the row with the index
row.Background = Brushes.LightGoldenrodYellow; //repaint
}));
}
The problem: Everything works fine, but sometimes when the program reloads, the line flashes per second and then highlighted back, and sometimes it's not painting it at all (untill the next reload).
I can't understand why this is happening. I think maybe the FindSelectionObject() begins to run before the loadTaskList() ends to invoke all and add the new objects into the datagrid.
But if so - Why? And how can I fix it?
In the bottom line, I want that after every reload the line re-paint immediately..
Thanks for any advice!

A few things to think about:
You should keep in mind that the DataGrid uses virtualization, which means that each item in your items source does not get its very own UI element. The UI elements are created to fill the visible area, and then re-used depending on which data-source item is currently bound to each one (this changes when you scroll for instance or change the items source). This may cause you problems in the future if you use your current approach, so keep this in mind.
The other thing is that the DataGrid may require more "cycles" of the layout process in order to update its UI. You may simply be calling FindSelectionObject prematurely. You have queued FindSelectionObject right after the invocation in loadTaskList. If the DataGrid needs to perform some actions which are queued on the dispatcher after the items source has changed, these will execute after the invocation in FindSelectionObject.
Try this instead:
private void loadTaskList() //Call every X time
{
List<myObject> myList = myquery();
this.Dispatcher.Invoke((Action)(() =>
{
TaskListTable.Items.Clear(); //Clear the DataGrid
foreach (myObject O in myList) //Add the items from the new query.
{
TaskListTable.Items.Add(O);
}
// The items of the grid have changed, NOW we QUEUE the FindSelectionObject
// operation on the dispatcher.
FindSelectionObject(); // <-- (( MOVE IT HERE )) !!
}));
}
EDIT: OK, so if this fails then maybe this will cover the case in which the above solution fails: subscribe to the LoadingRow event of DataGrid and set the appropriate background color if the row is the selected one. So in the cases when new rows are created this event will be called (due to virtualization it is not called per item in items source, but per actual row UI element). In the event args you will have access to the created DataGridRow instance.

I think this issue could be a visual thread synchronization. For this you can create and use a method similar like this:
public void LockAndDoInBackground(Action action, string text, Action beforeVisualAction = null, Action afterVisualAction = null)
{
var currentSyncContext = SynchronizationContext.Current;
var backgroundWorker = new BackgroundWorker();
backgroundWorker.DoWork += (_, __) =>
{
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US");
Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("en-US");
currentSyncContext.Send((t) =>
{
IsBusy = true;
BusyText = string.IsNullOrEmpty(text) ? "Espere por favor..." : text;
if (beforeVisualAction != null)
beforeVisualAction();
}, null);
action();
currentSyncContext.Send((t) =>
{
IsBusy = false;
BusyText = "";
if (afterVisualAction != null)
afterVisualAction();
}, null);
};
backgroundWorker.RunWorkerAsync();
}
IsBusy and BusyText are particular properties, that you can remove. The action variable will be the action to do in background (load your items for instance). beforeVisualAction and afterVisualAction are the visual actions you may want to do before and after the background action. Here are any visual update, for instance select your item, change color, set a view model variable that raise a binding update,... (any action that update the view).
Hope this method helps.

Are you maintaining the reference to lastSelectionObject somewhere? You say you're adding new objects, if they are truly new then the reference will be different and the reference comparison happening in IndexOf will not find it.

Related

Binding a DataGrid to a large IList dataset

I'm trying to use System.Windows.Controls.GridView (specifically, it is wrapped by Eto.Forms.GridView) to display a large dataset (1m+ rows) and finding it is unusable.
From what I can see, when the GridView.ItemsSource property is set, the grid immediately calls GetEnumerator() and therefore causes a large lag before it can display the enumerated dataset. As such, I have implemented a workaround to quickly display the grid using the code shown below.
Basically what the code attempts to do is override the usual List.GetEnumerator() functionality and initially give a small chunk of rows from the underlying list. After that, it utilizes the INotifyCollectionChanged.CollectionChanged event to add the remaining rows, chunks at a time.
While the solution works as far as displaying the grid relatively quickly on the initial load, there are a number of problems including:
As the list is populated via the thread, it becomes very unresponsive and of course the aesthetics of seeing the scroll-bar extending doesn't look great and;
The biggest of issue is that the grid becomes entirely unresponsive for a minute (+) each time you attempt to scroll down.
Does anyone know how I can make a DataGrid work with a large IList datasource? For the record, I cannot change controls as I am using ETO.Forms for cross-platform desktop UI capability.
Thanks.
// The underlying IList containing a large list of rows
IList<T> _underlyingList;
public IEnumerator<T> GetEnumerator() {
// Create an initial chunk of data to immediately return to the grid for display
long i = 0;
for (i = 0; i < _underlyingList.Count; i++) {
yield
return _underlyingList[i];
if (i > 100) break;
}
// Record the UI context so we can update the collection in that thread
var uiContext = SynchronizationContext.Current;
// Now we create a task that will populate the rest of the grid by
// raising "CollectionChanged" events to add the remaining rows.
Task.Run(() => {
// Create a temporary list to add to
var list = new List <T> ();
// Add to our list
for (long x = i; x < _underlyingList.Count; x++) {
list.Add(_underlyingList[x]);
// Every x items, fire a "CollectionChanged" event.
if (x % 1000 == 0) {
var e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, list);
// Invoke the CollectionChanged event on the UI thread
uiContext.Send(p => CollectionChanged?.Invoke(this, e), null);
list.Clear();
}
}
// Fire any last event as required.
if (list.Count > 0) {
var e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, list);
CollectionChanged?.Invoke(this, e);
uiContext.Send(p => CollectionChanged?.Invoke(this, e), null);
}
});
}```

System.ArgumentOutOfRangeException: 'Index must be within the bounds of the List. when adding a new item to ObservableCollection

This happens on a Xamarin.Forms app.
It is a simple app about making lists.
I got two pages: one with the lists and another to show the list's items. The error happens on the latter when I try to add a new item to an ObservableCollection.
This is a simplified version of the ViewModel:
public ObservableCollection<ListItem> Items { get; }
public Command AddItemCommand { get; }
public ItemsViewModel()
{
Items = new ObservableCollection<ListItem>();
AddItemCommand = new Command(OnAddItem);;
}
private async void OnAddItem()
{
await Device.InvokeOnMainThreadAsync(async () =>
{
if (string.IsNullOrEmpty(NewItemText))
return;
ListItem listITem = new ListItem()
{
ListId = _currentList.ListId,
Id = Guid.NewGuid().ToString(),
Text = NewItemText
};
Items.Add(listITem);
_currentList.ListItems.Add(listITem);
await DataStore.UpdateItemAsync(_currentList);
NewItemText = string.Empty;
});
}
The error happens on the Items.Add(listITem); call.
Tried wrapping the call on Device.InvokeOnMainThreadAsync with no luck.
The curious thing is it just happens on the second time I access the page.
The full project can be found on my GitHub:
https://github.com/JeffersonAmori/ListApp
It wouldn't surprise me if the error stopped happening if you lessen the chance for XForms to intervene before you've done the Add. (I'm hypothesizing that the underlying problem is a latent XForms bug.):
private async void OnAddItem()
{
// --- ASSUME we are already on MainThread. ---
// --- Avoid "await" (and any "..Invoke..Async") until after "Items.Add". ---
if (string.IsNullOrEmpty(NewItemText))
return;
ListItem listITem = new ListItem()
{
ListId = _currentList.ListId,
Id = Guid.NewGuid().ToString(),
Text = NewItemText
};
Items.Add(listITem);
_currentList.ListItems.Add(listITem);
NewItemText = string.Empty;
// ----- await Potentially slow operation(s) AFTER all quick UI calls. -----
await DataStore.UpdateItemAsync(_currentList);
}
CAVEAT #1: This doesn't fix any underlying problem, it just might make it happen less often. If its an XF problem, you might have to wrap your code in try..catch. In catch, determine if the item got added. If not try adding it again. Messy.
CAVEAT #2: This assumes OnAddItem is only called from MainThread. That will be true, if you never call it directly yourself - UI code will invoke the command on main thread.
CAVEAT #3: Assumes that all of the types involved (especially ListItem and _currentList.ListItems) are not UI types - they have no dependencies on Xamarin.Forms View classes.
I think the problem was the way the app was navigating between pages.
Before - when the error occurred - it was navigating using flyout routes via the flyout menu. Now I'm explicit registering the routes on code behind and pushing them into the navigation stack programmatically when the user clicks one of the items on the ColletcionView. This way I'm not reusing pages anymore.
public AppShell()
{
InitializeComponent();
Routing.RegisterRoute(nameof(ItemsPage), typeof(ItemsPage));
}
and then
async private void OnListSelected(List list)
{
if (list == null)
return;
await Shell.Current.GoToAsync($"{nameof(ItemsPage)}?{nameof(ItemsViewModel.ListId)}={list.ListId}");
}
This way I seems to get the threads to behavior correctly and always get a new list when navigating between pages as they are pushed/popped into/from the navigation stack.
Bear in mind there's some guess work on this answer as I'm just getting back into Xamari.Forms and I'm fairly new to Shell.
Thanks again toolmakersteve for the insights.
Cheers.

Cross-thread work with Control which have a data source (ComboBox)

At first I will post the code, it is short and quite clear.
cb_currentProfile is a ComboBox filled with 3 items when form is loaded:
delegate void SetCurrentProfileCallback(int index);
private void SetCurrentProfile(int index) // Set index of Combobox.SelectedItem
{
if (this.cb_currentProfile.InvokeRequired)
{
SetCurrentProfileCallback d = new SetCurrentProfileCallback(SetCurrentProfile);
this.Invoke(d, new object[] { index });
}
else
{
this.cb_currentProfile.SelectedItem = 2; // Won't work
this.cb_currentProfile.Visible = false; // It works
}
}
The problem is that when I try to change SelectedItem property, then it won't do nothing (no crash, just nothing happens).
I am sure that this code is reached in my form application.
At now I am making it in .NET 4.6 (but it was not working in v4.5 either)
The place where I am calling this method is in Task body:
Task.Run(() =>
{
while(true)
{
// ...
SetCurrentProfile(2);
// ...
Thread.Sleep(100);
}
});
I think that the problem is related to DataSource that seems to be invisible by other thread than main UI's.
I am also sure that data are loaded to ComboBox before code reaches a Task creation.
Edit 1 - selected item is null, Count property returns 0
When I used a debugger to check for some data, the results are:
var x = this.cb_currentProfile.SelectedItem; // null
var y = this.cb_currentProfile.Items.Count; // 0
It looks like, with the this.cb_currentProfile.SelectedItem = 2 statement, you intend to set the selection of the ComboBox by index. The ComboBox.SelectedItem Property accepts an Object and attempts to find it in its collection of items, selecting it if successful, and doing nothing otherwise. To select a particular index of the ComboBox, set the ComboBox.SelectedIndex Property instead.

Having trouble adding to an ObservableCollection using Dispatcher of UI thread

There are a lot of questions on here about updating controls(mainly the ObservableCollection) using a background worker thread. I am trying to implement this solution. However, my situation has just a little bit more depth to it because it calls to a function in the add method that returns the object to be added.
This is what my solution looks like right now (note that my ObservableCollection is bound to a TreeView control):
//Pass from Background Thread
MainTreeViewModel.AddLocation(locationName, locationValue);
//UI Thread (MainTreeViewModel)
public void AddLocation(MultiItemString displayName, int locationValue)
{
var node = Data.GetAllChildren(x => x.Children).Distinct().ToList().First(x => x.identify == 'P'); //Location Parent
App.Current.Dispatcher.BeginInvoke((Action)delegate()
{
node.Children.Add(CreateLocationNode(displayName, locationValue));
});
}
CreateLocationNode:
//Also location in MainTreeViewModel
private HierarchicalVM CreateLocationNode(MultiItemString displayName, int locationValue)
{
MultiItemString locationNumber = new MultiItemString(new[] {locationValue.ToString() + " "}, false);
var newLocationNode = new HierarchicalVM()
{
DisplayName = displayName, //Examples of props that turn out as NULL
LocationNumber = locationNumber,
TreeView_LocValue = locationValue,
Children = new ObservableCollection<HierarchicalVM>(), //This is what is being added to
Commands =
};
return newLocationNode;
}
When doing this I find that an object gets added, but all of the properties attached to it receive null values. Oppositely, when I am doing everything in the UI thread and just using node.Children.Add(CreateLocationNode(displayName, locationValue));, everything attaches how it should. Why am I getting a different result here, and how can I fix it?
Please let me know if you need more code.

WPF ListView: Changing ItemsSource does not change ListView

I am using a ListView control to display some lines of data. There is a background task which receives external updates to the content of the list. The newly received data may contain less, more or the same number of items and also the items itself may have changed.
The ListView.ItemsSource is bound to an OberservableCollection (_itemList) so that changes to _itemList should be visible also in the ListView.
_itemList = new ObservableCollection<PmemCombItem>();
_itemList.CollectionChanged += new NotifyCollectionChangedEventHandler(OnCollectionChanged);
L_PmemCombList.ItemsSource = _itemList;
In order to avoid refreshing the complete ListView I do a simple comparison of the newly retrieved list with the current _itemList, change items which are not the same and add/remove items if necessary. The collection "newList" contains newly created objects, so replacing an item in _itemList is correctly sending a "Refresh" notification (which I can log by using the event handler OnCollectionChanged of the ObservableCollection`)
Action action = () =>
{
for (int i = 0; i < newList.Count; i++)
{
// item exists in old list -> replace if changed
if (i < _itemList.Count)
{
if (!_itemList[i].SameDataAs(newList[i]))
_itemList[i] = newList[i];
}
// new list contains more items -> add items
else
_itemList.Add(newList[i]);
}
// new list contains less items -> remove items
for (int i = _itemList.Count - 1; i >= newList.Count; i--)
_itemList.RemoveAt(i);
};
Dispatcher.BeginInvoke(DispatcherPriority.Background, action);
My problem is that if many items are changed in this loop, the ListView is NOT refreshing and the data on screen stay as they are...and this I don't understand.
Even a simpler version like this (exchanging ALL elements)
List<PmemCombItem> newList = new List<PmemCombItem>();
foreach (PmemViewItem comb in combList)
newList.Add(new PmemCombItem(comb));
if (_itemList.Count == newList.Count)
for (int i = 0; i < newList.Count; i++)
_itemList[i] = newList[i];
else
{
_itemList.Clear();
foreach (PmemCombItem item in newList)
_itemList.Add(item);
}
is not working properly
Any clue on this?
UPDATE
If I call the following code manually after updating all elements, everything works fine
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
But of course this causes the UI to update everything which I still want to avoid.
After a change, you can use the following to refresh the Listview, it's more easy
listView.Items.Refresh();
This is what I had to do to get it to work.
MyListView.ItemsSource = null;
MyListView.ItemsSource = MyDataSource;
I know that's an old question, but I just stumbled upon this issue. I didn't really want to use the null assignation trick or the refresh for just a field that was updated.
So, after looking at MSDN, I found this article:
https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.inotifypropertychanged?redirectedfrom=MSDN&view=netframework-4.7.2
To summarize, you just need the item to implement this interface and it will automatically detect that this object can be observed.
public class MyItem : INotifyPropertyChanged
{
private string status;
public string Status
{
get => status;
set
{
OnPropertyChanged(nameof(Status));
status = value;
}
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
So, the event will be called everytime someone changes the Status. And, in your case, the listview will add a handler automatically on the PropertyChanged event.
This doesn't really handle the issue in your case (add/remove).
But for that, I would suggest that you have a look at BindingList<T>
https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.bindinglist-1?view=netframework-4.7.2
Using the same pattern, your listview will be updated properly without using any tricks.
You should not reset ItemsSource of ListView each time observable collection changed. Just set proper binding that will do your trick. In xaml:
<ListView ItemsSource='{Binding ItemsCollection}'
...
</ListView>
And in code-behind (suggest to use MVVM) property that will be responsible for holding _itemList:
public ObservableCollection<PmemCombItem> ItemsCollection
{
get
{
if (_itemList == null)
{
_itemList = new ObservableCollection<PmemCombItem>();
}
return _itemList;
}
}
UPDATE:
There is similar post which most probably will Answer your question: How do I update an ObservableCollection via a worker thread?
I found a way to do it. It is not really that great but it works.
YourList.ItemsSource = null;
// Update the List containing your elements (lets call it x)
YourList.ItemsSource = x;
this should refresh your ListView (it works for my UAP :) )
An alternative on Xopher's answer.
MyListView.ItemsSource = MyDataSource.ToList();
This refreshes the Listview because it's a other list.
Please check this answer:
Passing ListView Items to Commands using Prism Library
List view Items needs to notify about changes (done is setter)
public ObservableCollection<Model.Step> Steps
{
get { return _steps; }
set { SetProperty(ref _steps, value); }
}
and UpdateSourceTrigger need to be set in xaml
<Image Source="{Binding ImageData, UpdateSourceTrigger=PropertyChanged}" />

Categories

Resources