Reputation: 17445
I reckon the problem of C# win forms not being repainted well in certain circumstances is covered in different places, however, I didn't manage to solve my problems by using the simple snippets I found on the web.
My problem : on a form I have a listView, which I associate to a custom data holder (2 columns, a key and a last update date). From diverse places, I need to call the updateTime(key) method which then replicated the changes in the GUI. The model gets changed but my listView never.
I have a form containing a ListView that looks like that :
partial class VolsPane : UserControl, IGUIPane
{
private ListView listView1;
private ListModel listModel1; //ListModel is 100% homemade
...
public VolsPane()
{
...
listModel1.setList(listView1);
}
}
And the class holding data for my listView is like that :
class ListModel
{
private Dictionary<string, DateTime> Underlying;
private ListView list;
...
public ListModel(string nexusKey)
{
...
}
...
public void setList(ListView list)
{
this.list = list;
}
public void updateTime(string ric)
{
Underlying[ric] = DateTime.UtcNow;
updateView();
}
public void updateView()
{
this.list.Clear();
this.list.Items.AddRange(this.underlyingToListItems());
}
...
public ListViewItem[] underlyingToListItems()
{
ListViewItem[] res = new ListViewItem[Underlying.Keys.Count];
int i = 0;
foreach (string ric in Underlying.Keys)
{
res[i] = new ListViewItem(new string[] { ric, Underlying[ric].ToString("MMM-dd hh:mm:ss") });
i++;
}
return res;
}
}
I do realize the problem is in my updateView(). In debug, the code goes there definitely. Believing the problem was to be solved with an asynchronous "invoke", I referred to this post which appeared to be a reference : Stack overflow : Automating the invoke...
Then tried this :
private void updateView()
{
if (this.list.InvokeRequired)
{
this.list.Invoke(new MethodInvoker(() => { updateView(); }));
}
else
{
this.list.Items.Clear();
//this.list.Clear();
this.list.Items.AddRange(this.underlyingToListItems());
}
}
It builds but has no effect. In debug mode, never goes in the 'if' branch, always the 'else'.
Then this :
private void updateView()
{
this.list.Invoke((MethodInvoker)delegate
{
this.list.Items.Clear();
//this.list.Clear();
this.list.Items.AddRange(this.underlyingToListItems());
});
}
I get an "InvalidOperationException : Invoke or BeginInvoke cannot be called on a control until the window handle has been created."
What is the obvious thing I must be missing here ? Or is my problem actually not the one i think ?
Thanks guys !
Upvotes: 3
Views: 2954
Reputation: 1269
You are correct, the problem lies in the updateView() code. You do need to Invoke to the UI thread but the issue is the handle has not yet been created for the control. One gotcha when working with WinForms is that the InvokeRequired will actually return false if the handle has not yet been created. See this explanation form the MSDN documentation:
If the control's handle does not yet exist, InvokeRequired searches up the control's parent chain until it finds a control or form that does have a window handle. If no appropriate handle can be found, the InvokeRequired method returns false.
That's why your check for InvokeRequired was always failing. I've seen this problem fixed in a couple of ways. One solution is to attach a callback to the Control's handle created event:
public class HandleHookedListView: ListView
{
private EventHandler _handleCreatedEvent;
public HandleHookedListView(): base()
{
_handleCreatedEvent = new EventHandler(HandleHookedControl_HandleCreated);
this.HandleCreated += _handleCreatedEvent;
}
private bool _handleIsCreated;
public bool HandleIsCreated
{
get { return _handleIsCreated; }
set { _handleIsCreated = value; }
}
void HandleHookedControl_HandleCreated(object sender, EventArgs e)
{
Debug.Print("Handle Created");
this.HandleIsCreated = true;
// Unhook the delegate
if (_handleCreatedEvent != null)
this.HandleCreated -= _handleCreatedEvent;
}
}
You will then have to modify your updateView to check if the handle has been created. In this case your instance of ListView (list) has been replaced with the new HandleHookedListView
private void updateView()
{
var handleCreated = this.list.HandleIsCreated;
if (this.list.InvokeRequired && handleCreated)
{
// Handle is created and invoke is required.
this.list.Invoke(new MethodInvoker(() => { updateView(); }));
}
else if (handleCreated)
{
// In this case your control's handle has been created and invoke really
// isn't required go ahead and do the update
this.list.Items.Clear();
this.list.Items.AddRange(this.underlyingToListItems());
}
else
{
// You cannot update yet. The handle has not been created. Depending on if
// you need to "queue" these updates you can either collect them or just
// ignore them if a subsequent call to updateView() after the handle has been
// created will be sufficient
}
}
The real key here is that you are trying to do an update to the control before it has been fully initialized.
Upvotes: 3
Reputation: 1629
First of all
Doesn't build, invoke doesn't exist for my list model.
As far as I remember Invoke is the method of Control class. So you can't call it in ListModel class without any instance of class inherited from Control. Use
this.list.Invoke(
In debug mode, never goes in the 'if' branch, always the 'else'.
This may mean that this.list.InvokeRequired was called in GUI thead.
But it is also may mean that list.InvokeRequired was called before this.list was painted at least once. This is tricky moment. If instance of Control class hasn't painted yet than gdi+ (or what is under the hood of C# WinForm painting) has not initialized yet. So nothing to synchronize. Please double check this.
Upvotes: 1