Having trouble adding to an ObservableCollection using Dispatcher of UI thread - c#

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.

Related

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.

Creating static Windows.Forms.Control

I have a project to which I delegate the function of creating a library of (static)? classes used in all my other projects. It is referenced via solution in these cases.
For instance, I have an extension which creates checkboxes within a given GroupBox's panel, and that works great:
public static void PreencheCheckboxesPanel(this Panel p, List<CheckBox> listaCheckBoxs) {
var count = 0;
listaCheckBoxs.ForEach(
i => {
i.Location = new Point(10, 10 + ((count) * 25)); //"dynamic" and not-so-effective resizing here
i.AutoSize = true;
count++;
});
p.Controls.AddRange(listaCheckBoxs.ToArray());
}}
Problem is, I need to insert a static checkbox on the top of the list, which will receive a method to (un)?check all the checkboxes below.
So my code will become
internal static CheckBox CKB_ancora = new CheckBox(){};
public static void PreencheCheckboxesPanel(this Panel p, List<CheckBox> listaCheckBoxs) {
var count = 0;
if (adicionaAncora) {
CKB_ancora.Text = textoAncora;
CKB_ancora.CheckedChanged += (sender, args) => {
ChecaCheckBoxes(p, CKB_ancora.Checked);
};
listaCheckBoxs.Insert(0, CKB_ancora);
}
listaCheckBoxs.ForEach(
i => {
i.Location = new Point(10, 10 + ((count) * 25)); //"dynamic" and not-so-effective resizing here
i.AutoSize = true;
count++;
});
p.Controls.AddRange(listaCheckBoxs.ToArray());
}}
where ChecaCheckBoxes is another
public static void ChecaCheckBoxes(this Panel b, bool checkStatus = true) {
var listaCheckBoxs = (from Control c in b.Controls where c is CheckBox select (CheckBox)c).ToList();
listaCheckBoxs.ForEach(
i => {
i.Checked = checkStatus;
});
}
and CKB_ancora needs to be a solution-wide recognized object.
The reason? I have another extension named GetSelectedCheckBoxes which will be used to return all the checked ... ah... checkboxes within the groupbox. And, in order to make sure that the "anchor" (I call it like this, since I don't have a name to a (un)?check-all checkbox) won't be returned as well.
If I run this code, it will compile, but... will run accross an InvalidOperationException at Application.SetCompatibleTextRenderingDefault, right at Main(); Apparently, a control cannot be created/instantied before this method is run at mainpoint, which is the exact definition of "static".
Question: Knowing that I NEED a way to keep this particular check solution-wide visible... How do I do it?
Unfortunately, you have not provided a good, minimal, complete code example, and lacking enough context it is very hard to provide good, specific advice.
That said, it seems that somewhere in this static class of yours, you have a field named CKB_ancora, and you are probably initializing it using a field initializer, possibly like this:
private CheckBox CKB_ancora = new Checkbox();
And having done this, you are finding that when the class is initialized (typically on the first time something in the class is accessed at runtime), that happens too soon and an exception is thrown.
Assuming that's correct, then it seems to me that most obvious and simplest "fix" is to initialize the object lazily. For example:
private Lazy<CheckBox> _ckb_ancora =
new Lazy<CheckBox>(() => new CheckBox());
private CheckBox CKB_ancora { get { return _ckb_ancora.Value; } }
That will wrap the object storage in a property, which in turn uses an instance of Lazy<T> to defer initialization until the first time any code actually tries to access it.
Now, that said, I'm not very enamored of your approach here, with a static member that is used in some instantiated object. What if someone using your library wants to use the code with two or more Panel instances? A Control (including a CheckBox) can't be a child of more than one other Control at a time, so having a single static instance of the Control is just not going to work.
IMHO, it would probably be better to instead use some identifying feature such as the Name or Tag property of the CheckBox to handle the control appropriately (e.g. such as filtering it out of enumerations).
For example:
public static void PreencheCheckboxesPanel(this Panel p, List<CheckBox> listaCheckBoxs) {
var count = 0;
if (adicionaAncora) {
CheckBox CKB_ancora = new CheckBox();
CKB_ancora.Text = textoAncora;
CKB_ancora.Name = "CKB_ancora";
CKB_ancora.CheckedChanged += (sender, args) => {
ChecaCheckBoxes(p, CKB_ancora.Checked);
};
listaCheckBoxs.Insert(0, CKB_ancora);
}
listaCheckBoxs.ForEach(
i => {
i.Location = new Point(10, 10 + ((count) * 25)); //"dynamic" and not-so-effective resizing here
i.AutoSize = true;
count++;
});
p.Controls.AddRange(listaCheckBoxs.ToArray());
}}
And, for example:
public static void ChecaCheckBoxes(this Panel b, bool checkStatus = true) {
var listaCheckBoxs = b.Controls
.OfType<CheckBox>().Where(c => c.Name != "CKB_ancora").ToList();
listaCheckBoxs.ForEach(
i => {
i.Checked = checkStatus;
});
}
That way, when you go to retrieve the list of CheckBox controls, the special one you added at the beginning is ignored.
Even with that, I think the code is still pretty fragile. The above would work better, but IMHO it would probably be even better if you weren't using static members for all of this in the first place. I.e. instead you should design some mechanism that allows you to relate an instance of the helper class to the Panel object it's helping with, so that you can in fact initialize and store per-instance information, without running into problems of order of execution, as well as of limitations of use of the code with just a single client.
Without a better code example, I don't see any good way to offer any specific advice along those lines though.

Wpf application and Threads

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.

UI freezing when altering ObservableCollection/CollectionViewSource

I have a datagrid that is receiving an update of 100,000+ rows which is bound to a CollectionViewSource.View whose source is an ObservableCollection. If I try to update in the current setup the UI freezes completely when I'm adding the new rows to the ObservableCollection. To work around this I set the CollectionViewSource.Source = null before adding the rows to the ObservableCollection which allows it to work fine. The only problem is that once I do this for the first load the next load will still have the UI freezing problem.
Here's the code.
public CollectionViewSource ViewSource { get; set; }
private ObservableCollection<ScreenerRow> internalRows = new ObservableCollection<ScreenerRow>();
private async Task Search()
{
internalRows.Clear();
ViewSource.Source = null;
var data = await Task.Run(() =>
{
return DoStuff();
});
if (data == null) return;
foreach (ScreenerRow sRow in data)
{
//freezes in here on second run
internalRows.Add(sRow);
}
ViewSource.Source = internalRows;
}
Just wondering if anyone else has had this problem or see's an issue with the way I am doing this.
Thanks for the help.
Edit: Changing my ObservableCollection to a List allows this to work fine.
//private ObservableCollection<ScreenerRow> internalRows = new ObservableCollection<ScreenerRow>();
private List<ScreenerRow> internalRows = new List<ScreenerRow>();
I take it from your edit that this may not be the case, but if there are cases where you actually need the collection to be observable, then you can also use the CollectionViewSource.DeferRefresh() method to prevent the UI from being updated after each individual change:
private async Task Search()
{
internalRows.Clear();
using(ViewSource.DeferRefresh())
{
var data = await Task.Run(() => GetSearchResults());
if (data == null) return;
foreach (ScreenerRow sRow in data)
{
internalRows.Add(sRow);
}
}
}
And also make sure that you have the virtualization mode set correctly on the control.

Categories

Resources