I had a regular Forms.ListView and converted it to Virtual list. Implemented the RetrieveVirtualItem and everything was working fine.
Then I decided that I'll add cache and eventually I'll need sorting and who knows what else. Since I've inherited the code and it was already somewhat messy, I decided to yank my changes and move them to a separate class, namely: derive from ListView, e.g. class MyOwnListView : ListView
so I've moved it and also added CacheVirtualItems.
After implementing both of these methods, I replaced:
private System.Windows.Forms.ListView someListView;
with
private MyOwnListView someListView;
on the main form.
So far so good..it runs, doesn't crash, however (and I only have about 60 items for now..) when I move the scroll bar, a lot of them don't repaint, so you'll see empty white rows, and sometimes it will repaint/show after clicking on that row. I also get partially displayed and drawn row, such as the top of the row won't be displaying in its entirety.
I'm not sure what the problem is, I tried adding DoubleBuffered=true;
I also added to my constructor the following: (per some suggestion I've found somewhere here and/or googling..)
SetStyle( ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true );
SetStyle( ControlStyles.EnableNotifyMessage, true );
and this method:
protected override void OnNotifyMessage( Message m )
{
//Filter out the WM_ERASEBKGND message
if ( m.Msg != 0x14 )
{
base.OnNotifyMessage( m );
}
}
My code overall is very similar to this:..just to give you an idea:
public class MyListView: ListView
{
private ListViewItem[] cache;
private int firstItem;
public MyListView()
{
SetStyle( ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true );
SetStyle( ControlStyles.EnableNotifyMessage, true );
RetrieveVirtualItem += new RetrieveVirtualItemEventHandler( xxx_RetrieveVirtualItem );
CacheVirtualItems += new CacheVirtualItemsEventHandler( xxx_CacheVirtualItems );
}
private void xxx_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
{
if (cache != null && e.ItemIndex >= firstItem && e.ItemIndex < firstItem + cache.Length)
e.Item = cache[e.ItemIndex - firstItem];
else
e.Item = GetItem(e.ItemIndex);
}
private void xxx_CacheVirtualItems(object sender, CacheVirtualItemsEventArgs e)
{
if (cache != null && e.StartIndex >= firstItem && e.EndIndex <= firstItem + cache.Length)
return;
firstItem = e.StartIndex;
int length = e.EndIndex - e.StartIndex + 1;
cache = new ListViewItem[length];
for (int i = 0; i < cache.Length; i++)
cache[i] = GetItem(firstItem + i);
}
Now, GetItem, basically accesses a List<someobject>, gets the object out of list, and based on that
it creates new ListViewItem and returns it.
}
EDIT: I've added some debugging code to the xxx_CacheVirtualItems. And it appears that everytime I scroll it comes back that the item was not found and is adding it again to the cache. Not sure why yet. I would expect that after the first scroll all the way down it would keep them inside the cache. I'm still looking.
I also tried adding:
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
and tried //Application.SetCompatibleTextRenderingDefault(true);
That application didn't have these at all..these two lines did something interesting. SetCompatibleTextRenderingDefault with false, did not scroll at all, while SetCompatibleTextRenderingDefault with true did scroll, but only down and once I've reach the bottom it stopped. but the screen didn't have any painting/refresh problems..
OK Folks, I figured it out. It's very weird, but
I inadvertently changed some code, and didn't think it was a big deal, let me paraphrase it, so in my GetItem(index); code
What I had was:
ListViewItem v = new ListViewItem(....,0);
v.SubItems.Add(...);
v.SubItems.Add(...);
v.SubItems.Add(...);
......
...
well, I wanted to be "elegant" I've changed that to:
ListViewItem v = new ListViewItem(....,0);
v.SubItems.AddRange( new string[] { prop1, prop2, prop3, , , ,....});
And that was the issue, I just changed it back and magically it started to work.
Related
I wanted to implement a MiddleClick-To-Delete functionality, like most tabbed environments have, on my application which looks like this:
It's just a single form with a TabControl that has 2x TabPage: ActiveItems and Archived Items.
Both of those pages contain their own TabControl, which the user can add as many TabPage objects as they desire.
If the user has Active Items selected and they delete a tab, it deletes the correct one. If they delete a tab from Archived Items, it also deletes the correct one. If they move a tab from Active to Archived then delete it, it is removing the wrong tab. So in the screenshot above, you can't see Archived Items but there are 2 tabs there already. If I move 13571 from Active -> Archive, then delete it (tab #3, index #2), it removes tab #2, index #1.
// Both the tcActive and tcArchived, the TabControls on tcRoot's two Active Items
// and Archived Items tab pages, subscribe to this event
private void tc_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button != System.Windows.Forms.MouseButtons.Left)
tabPageClicked = e.Location;
if (e.Button == MouseButtons.Middle)
DeleteTab();
}
// Passes the TabControl and TabPageIndex to be deleted
private void DeleteTab()
{
int tpi = GetTabIndex(tabPageClicked);
if (tcActiveArchive.SelectedIndex.Equals(0))
DeleteTab(tcActive, tpi);
else
DeleteTab(tcArchived, tpi);
}
// Gets the TabPage index from either Active Items page or Archived Items page
private int GetTabIndex(Point p)
{
if (tcActiveArchive.SelectedIndex.Equals(0))
return tcActive.TabPages.IndexOf(tcActive.TabPages.Cast<TabPage>().Where((t, i) => tcActive.GetTabRect(i).Contains(p)).First());
else
return tcArchived.TabPages.IndexOf(tcArchived.TabPages.Cast<TabPage>().Where((t, i) => tcArchived.GetTabRect(i).Contains(p)).First());
}
// Actual removal of TabPage at index tabIndex from TabControl tc
private void DeleteTab(TabControl tc, int tabIndex)
{
lastDeleteWasActiveTab = (tcActiveArchive.SelectedIndex.Equals(0));
//Tab object just stores a string, string[] and bool
Tab deletedTab = new Tab(tc.TabPages[tabIndex].Text, ((TextBox)tc.TabPages[tabIndex].Controls[0]).Lines, lastDeleteWasActiveTab);
if (lastDeleteWasActiveTab)
{
deletedActiveTabs.Push(deletedTab);
filesToDelete.Add(mantisDir + tc.TabPages[tabIndex].Text + ".txt");
}
else
{
deletedArchiveTabs.Push(deletedTab);
filesToDelete.Add(archiveDir + tc.TabPages[tabIndex].Text + ".txt");
}
tc.TabPages.RemoveAt(tabIndex);
//Try to select the tab to the right of the one that was deleted
if (tc.TabPages.Count >= tabIndex + 1)
tc.SelectedIndex = tabIndex;
else
tc.SelectedIndex = tabIndex - 1;
undoQueue.Push((lastDeleteWasActiveTab) ? UndoEventType.DeleteActive : UndoEventType.DeleteArchive);
}
Stepping through the code for the condition described above:
MouseDown event fires
DeleteTab()
GetTabIndex returns 2
DeleteTab(tcArchived, 2); is called
Tab deletedTab has all the values it's supposed to contain, indicating we're looking at the right TabPage / index.
tc.RemoveAt(2) is called
Yet it deletes the second item, not the index 2. Absolutely cannot figure out what's wrong.
Changing this line:
DeleteTab(TabControl tc, int tabIndex)
{
//other code
tc.TabPages.RemoveAt(tabIndex);
}
To:
TabPage tp = tc.TabPages[tabIndex];
tc.TabPages.Remove(tp);
Seems like it works for the use case described in my original post, with minimal testing. But I'd really like to know why RemoveAt(tabIndex) isn't working.
I have some items in a CheckedListBox, I want to disable the CheckBox of first item in it.
i.e. I want to disable the first item in the CheckedListBox, because I want to tell the user visually that option is not available.
Combining 2 of the above partial answers worked great for me.
Add your items to the list with:
myCheckedListBox.Items.Add(myItem, myState);
Where myState is CheckState.Indeterminate for items that should be disabled.
Then add an event handler to keep those items from being changed:
myCheckedListBox.ItemCheck += (s, e) => { if (e.CurrentValue == CheckState.Indeterminate) e.NewValue = CheckState.Indeterminate; };
This does not allow you to use 'Indeterminate' in this list for its normal purpose but it does give a look very similar to what one would expect for a disabled item and it provides the correct behavior!
Though this post is pretty old, the last added answer has been submitted in April this year,
and I hope this will help someone.
I was after something similar : a checked list box that behaves like
a lot of installers, which offer a list of options where some features are required and
thus are both checked and disabled.
Thanks to this post (Can I use a DrawItem event handler with a CheckedListBox?)
I managed to do that, subclassing a CheckedListBox control.
As the OP in the linked post states, in the CheckedListBox control the OnDrawItem event is never fired,
so subclassing is necessary.
It's very basic, but it works.
This is what it looks like (the CheckBox above is for comparison) :
NOTE: the disabled item is really disabled : clicking on it has no effects whatsoever (as far as I can tell).
And this is the code :
public class CheckedListBoxDisabledItems : CheckedListBox {
private List<string> _checkedAndDisabledItems = new List<string>();
private List<int> _checkedAndDisabledIndexes = new List<int>();
public void CheckAndDisable(string item) {
_checkedAndDisabledItems.Add(item);
this.Refresh();
}
public void CheckAndDisable(int index) {
_checkedAndDisabledIndexes.Add(index);
this.Refresh();
}
protected override void OnDrawItem(DrawItemEventArgs e) {
string s = Items[e.Index].ToString();
if (_checkedAndDisabledItems.Contains(s) || _checkedAndDisabledIndexes.Contains(e.Index)) {
System.Windows.Forms.VisualStyles.CheckBoxState state = System.Windows.Forms.VisualStyles.CheckBoxState.CheckedDisabled;
Size glyphSize = CheckBoxRenderer.GetGlyphSize(e.Graphics, state);
CheckBoxRenderer.DrawCheckBox(
e.Graphics,
new Point(e.Bounds.X + 1, e.Bounds.Y + 1), // add one pixel to align the check gliph properly
new Rectangle(
new Point(e.Bounds.X + glyphSize.Width + 3, e.Bounds.Y), // add three pixels to align text properly
new Size(e.Bounds.Width - glyphSize.Width, e.Bounds.Height)),
s,
this.Font,
TextFormatFlags.Left, // text is centered by default
false,
state);
}
else {
base.OnDrawItem(e);
}
}
public void ClearDisabledItems() {
_checkedAndDisabledIndexes.Clear();
_checkedAndDisabledItems.Clear();
this.Refresh();
}
}
Use it like this:
checkedListBox.Items.Add("Larry");
checkedListBox.Items.Add("Curly");
checkedListBox.Items.Add("Moe");
// these lines are equivalent
checkedListBox.CheckAndDisable("Larry");
checkedListBox.CheckAndDisable(0);
Hope this can help someone.
Disabling items isn't a great idea, the user will have no good feedback that click the check box won't have any effect. You cannot use custom drawing to make it obvious. Best thing to do is to simply omit the item.
You can however easily defeat the user with the ItemCheck event:
private void checkedListBox1_ItemCheck(object sender, ItemCheckEventArgs e) {
if (e.Index == 0) e.NewValue = e.CurrentValue;
}
To disable any particular item use following:
checkedListBox1.SetItemCheckState(0, CheckState.Indeterminate);
SetItemCheckState takes index of item and CheckState Enum
Indeterminate is used to show shaded appearance
I know it has been a while, but I found this in my search for a list box and thought I would add it to the discussion.
If you have a listbox and want to disable all of the checkboxes so they cannot be clicked, but not disable the control so the user can still scroll etc. you can do this:
listbox.SelectionMode = SelectionMode.None
The CheckedListBox will not work in this way. CheckedListBox.Items is a collection of strings so they cannot be "disabled" as such.
Here are some discussions about possible solutions that might help you: here and here.
This works for me:
checkedListBox1.SelectionMode = SelectionMode.None;
Which means no items can be selected
None: No items can be selected.
For more info, you can check it here: SelectionMode Enumeration.
The solution is to use the event ItemChecking:
_myCheckedListBox.ItemChecking += (s, e) => e.Cancel = true;
This will cancel all the checking on every item, but you can always do more refined solution but testing the current .SelectedItem
Here's how I did it in a helpdesk application I wrote:
First, I made it so the check box was greyed out as I added it to the list during form load:
private void frmMain_Load(object sender, EventArgs e)
{
List<string> grpList = new List<string>();
ADSI objADSI = new ADSI();
grpList = objADSI.fetchGroups();
foreach (string group in grpList)
{
if (group == "SpecificGroupName")
{
chkLst.Items.Add(group, CheckState.Indeterminate);
}
else
{
chkLst.Items.Add(group);
}
}
Then I used an event so that when clicked it ensures it stays clicked:
private void chkLst_SelectedIndexChanged(object sender, EventArgs e)
{
if (chkLst.SelectedItem.ToString() == "SpecificGroupName")
{
chkLst.SetItemCheckState(chkLst.SelectedIndex, CheckState.Indeterminate);
}
}
The idea here is that on my form it's set so that the box checks on item click/select. This way I could kill two birds with one stone. I could keep this event from causing problems when the item is first checked and added during form load. Plus making it check on select allows me to use this event instead of the item checked event. Ultimately the idea is to keep it from messing up during the load.
You'll also notice that it doesn't matter what the index number is, that variable is unknown because in my app it's grabbing a list of groups from AD that exist in a specific OU.
As to whether this is a good idea or not, that's dependent on the situation. I have another app where the item to disable is dependent on another setting. In this app I just want the helpdesk to see that this group is required so they don't go removing them from it.
Try Below Code:
Private Sub CheckedListBox1_MouseUp(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles CheckedListBox1.MouseUp
If (Condition) Then
Me.CheckedListBox1.SelectedIndex = -1
End If
End Sub
I think an alternative solution, is using Telerik components.
A RadListControl can give you that option:
the problem is probably simple, the post is longer than I wished, but I've tried providing as much info and detail as possible.
I didn't write this GUI app, nor designed, however like most of us I've inherited it.
It had a (regular) ListView, actually the app has several ListView(s), not sure if that matters yet.
Because the # of items arriving to this one ListView (screen/form) can get very large 10K+ I decided to convert it to virtual list, however I'm experiencing some early problems.
One of the biggest problems, is that the items are being populated asynchronously by hitting a button on the form.
When they arrive (from service/network/database) the items are built into ListViewItem(s) and added to someListItems which is an ArrayList.
In my RetrieveVirtualItem method I need to handle both cases when the list is empty and when I already have something (after the button was hit) and that's when I hit the wall (no pun intended)
with the following line of code:
if ( someListItems.Count > e.ItemIndex )
It basically causes (no idea why) a call to Dispose method on the main form which results in the entire application crashing hard. BUT!!, it only happens when I click on the form and list. If the form is just loaded and populated it is fine..the second you left click on the mouse, BOOM!
It took my couple of hours to figure out that the line above was the culprit, as the call stack wasn't very apparent to point that out, and another minute to find out that e.ItemIndex is the culprit. But WHY??? I
n msdn examples they access e.ItemIndex to perform tests and it seems fine.
The Virtual Mode is set in the constructor of the form:
myListView.VirtualMode = true;
VirtualListSize is set right after data arrives asynchronously:
myListView.VirtualListSize = someArrayList.Count;
This is my RetrieveVirtualItem implementation:
private void blah_RetrieveVirtualItem( object sender, RetrieveVirtualItemEventArgs e )
{
// someListItems is an ArrayList that is created when the object/class loads..and populated with ListViewItems.
// i.e. private ArrayList someListItems = new ArrayList();
// it is populated asynchronously by hitting a button on the form, hence it's empty when the form loads..
if ( someListItems.Count <= 0 )
{
e.Item = new ListViewItem( "" );
e.Item.SubItems.Add( "" );
e.Item.SubItems.Add( "" );
}
else
{
// the of code below is the problem, and more specifically - e.ItemIndex causes somehow to call Dispose on the main form..
// the reason I have this code is because if I take it out, all items will show up, no problem, but it will crash when I try to scroll down..
// with message like this:
// Index was out of range. Must be non-negative and less than the size of the collection. Parameter name: index
if ( someListItems.Count > e.ItemIndex )
{
// took out my code out to eliminate possibility that it's my code. :)
int x = e.ItemIndex * e.ItemIndex;
e.Item = new ListViewItem( x.ToString() );
// but I had something like that just for a test:
// ListViewItem item = ( ListViewItem )someListItems[e.ItemIndex];
// e.Item = item;
// remember that someListItems already has ListViewItems
}
}
}
The method that gets called asynchronously, creates ListViewItems and populates someListItems looks something like that:
private void ExampleMethod_That_PopulatesSomeArrayList(ArrayList ar)
{
//Im only showing more essential code..
SomeArrayList.Items.Clear();
myListView.VirtualListSize = ar.Count;
foreach ( SomeObject o in ar )
{
ListViewItem lvi = new ListViewItem( SomeObject.somePropertyID, 0 );
// I've tried changing the above line to: lvi = new ListViewItem( SomeObject.somePropertyID, 0 ); // and having the ListViewItem lvi on the class level. i.e private ListViewItem lvi
// didn't help.. :(
lvi.SubItems.Add( o.someProperty1 );
lvi.SubItems.Add( o.someProperty2 );
// there's quite few of these subitems..2 is enough for this example...
}
// the orignal code, before I changed it to virtual list was adding the items somewhere here..after finished looping, now I'm just trying to reuse that array of ListViewItems.
}
There's also another problem that the items don't really show up at all unless I take out the:
if ( someListItems.Count > e.ItemIndex )
but then I experience the index of out of range issue when I try scrolling.
UPDATE:
I've noticed that if I set the size of virtual list, only after the loop is finished and therefore it is zero (0) at the beginning (I can always reset it to zero), then everything works and don't need to check for size, all I have to do is this:
After the loop in: private void ExampleMethod_That_PopulatesSomeArrayList(ArrayList ar)
this.myListView.VirtualListSize = someListItems.Count;
which I would like to thank for Hans Passant for noticing the discrepancy.
So this is the complete, for now (I'm sure that I'll add some code or change as I would like to add some caching, but at least I have something...
private void blah_RetrieveVirtualItem( object sender, RetrieveVirtualItemEventArgs e )
{
e.Item = ( ListViewItem )someListItems[e.ItemIndex];
}
The only thing I'm not sure what Hans Passant mentioned is this: "it really isn't okay for this event handler to never allocate a ListViewItem. " which I'm not sure if I understand, because the ListViewItems are allocated and inserted into someListItems array. I do have a try catch around, and I did before as well.
Also, I was thinking and I would appreciate someone's input on this idea:
Create a separate object that would hold all the properies of SomeObject or insert SomeObject(s) into the List and create new ListViewItems as required?
e.g:
private void blah_RetrieveVirtualItem( object sender, RetrieveVirtualItemEventArgs e )
{
// that list would be build sometime during the loop iteration in
// (I'm using the original method name mentioned way above in this post)
// ExampleMethod_That_PopulatesSomeArrayList(ArrayList ar)
SomeObject o = listOfObjects[e.ItemIndex];
e.Item = new ListViewItem();
e.Item.SubItems.Add(o.prop1);
e.Item.SubItems.Add(o.prop2);
e.Item.SubItems.Add(o.prop3);
}
To answer this question. The virtual list was crashing because the VirtualListSize wasn't being set correctly.
Basically, to help others here, if you have a virtual list, always make sure that the VirtualListSize corresponds to the actual number of items you're trying to show. If not, all hell breaks loose. If you do update, remove, add, anything, you need to reset VirtualListSize to the correct number.
I ended up deriving from ListView and storing my listviewitems in an array.
I'm displaying a set of search results in a ListView. The first column holds the search term, and the second shows the number of matches.
There are tens of thousands of rows, so the ListView is in virtual mode.
I'd like to change this so that the second column shows the matches as hyperlinks, in the same way as a LinkLabel shows links; when the user clicks on the link, I'd like to receive an event that will let me open up the match elsewhere in our application.
Is this possible, and if so, how?
EDIT: I don't think I've been sufficiently clear - I want multiple hyperlinks in a single column, just as it is possible to have multiple hyperlinks in a single LinkLabel.
You can easily fake it. Ensure that the list view items you add have UseItemStyleForSubItems = false so that you can set the sub-item's ForeColor to blue. Implement the MouseMove event so you can underline the "link" and change the cursor. For example:
ListViewItem.ListViewSubItem mSelected;
private void listView1_MouseMove(object sender, MouseEventArgs e) {
var info = listView1.HitTest(e.Location);
if (info.SubItem == mSelected) return;
if (mSelected != null) mSelected.Font = listView1.Font;
mSelected = null;
listView1.Cursor = Cursors.Default;
if (info.SubItem != null && info.Item.SubItems[1] == info.SubItem) {
info.SubItem.Font = new Font(info.SubItem.Font, FontStyle.Underline);
listView1.Cursor = Cursors.Hand;
mSelected = info.SubItem;
}
}
Note that this snippet checks if the 2nd column is hovered, tweak as needed.
Use ObjectListView -- an open source wrapper around a standard ListView. It supports links directly:
This recipe documents the (very simple) process and how you can customise it.
The other answers here are great, but if you don't want to have to hack some code together, look at the DataGridView control which has support for LinkLabel equivalent columns.
Using this control, you get all the functionality of the details view in a ListView, but with more customisation per row.
You can by inheriting the ListView control override the method OnDrawSubItem.
Here is a VERY simple example of how you might do:
public class MyListView : ListView
{
private Brush m_brush;
private Pen m_pen;
public MyListView()
{
this.OwnerDraw = true;
m_brush = new SolidBrush(Color.Blue);
m_pen = new Pen(m_brush)
}
protected override void OnDrawColumnHeader(DrawListViewColumnHeaderEventArgs e)
{
e.DrawDefault = true;
}
protected override void OnDrawSubItem(DrawListViewSubItemEventArgs e)
{
if (e.ColumnIndex != 1) {
e.DrawDefault = true;
return;
}
// Draw the item's background.
e.DrawBackground();
var textSize = e.Graphics.MeasureString(e.SubItem.Text, e.SubItem.Font);
var textY = e.Bounds.Y + ((e.Bounds.Height - textSize.Height) / 2);
int textX = e.SubItem.Bounds.Location.X;
var lineY = textY + textSize.Height;
// Do the drawing of the underlined text.
e.Graphics.DrawString(e.SubItem.Text, e.SubItem.Font, m_brush, textX, textY);
e.Graphics.DrawLine(m_pen, textX, lineY, textX + textSize.Width, lineY);
}
}
You can set HotTracking to true so that when the user hovers mouse over the item it appears as link.
Something strange is going on with ObservableCollection.
I have the following code:
private readonly ObservableCollection<DisplayVerse> _display;
private readonly ListBox _box;
private void TransferToDisplay()
{
double elementsHeight = 0;
_display.Clear();
for (int i = 0; i < _source.Count; i++) {
DisplayVerse verse = _source[i];
_display.Add(verse);
elementsHeight += CalculateItemsHeight(i);
if (elementsHeight + Offset > _box.ActualHeight) {
_display.RemoveAt(_display.Count - 1);
break;
}
}
MessageBox.Show(elementsHeight.ToString());
}
private double CalculateItemsHeight(int index)
{
ListBoxItem lbi = _box.ItemContainerGenerator.ContainerFromIndex(index) as ListBoxItem;
return lbi != null ? lbi.ActualHeight : 0;
}
What I am trying to do here is control how many items go into the ObservableCollection _display. Now, within this for loop you can see that elements are added until the total elements height (+offset) is greater than the listbox itself.
Now, this is strange, the elementsHeight equals 0 after this for loop. (CalculateItemsHeight returns 0 in all for loop iterations even though the lbi is not null) It seems that the UI elements defined in the datatemplate are not created...
Yet.
Now, if I put some MessageBoxes after the _display.Add(verse) you can see that the CalculateItemsHeight actually returns the height of an item.
for (int i = 0; i < _source.Count; i++) {
DisplayVerse verse = _source[i];
_display.Add(verse);
MessageBox.Show("pause"); // <----- PROBLEM?
elementsHeight += CalculateItemsHeight(i);
if (elementsHeight + Offset > _box.ActualHeight) {
_display.RemoveAt(_display.Count - 1);
break;
}
}
MessageBox.Show(elementsHeight.ToString());
After I modify the for loop as shown, the last MessageBox actually shows the actual height for all processed elements.
My question is - when are the UI elements actually created? It seems that it was done somewhere during the MessageBox display. This behaviour is pretty strange for me, maybe it has something to do with threading, not sure.
Adding to the _display ObservableCollection obviously creates an item immediately, but not its visual elements (they are however added afterwards, I just don't know exactly when). How can I do this same behaviour without having to pop the message box up?
Actually, I was trying to get this to work and I found the ".UpdateLayout()" function, which works perfectly for me. I realize that you're doing vertical and I'm doing horizontal, but here's my code, it's pretty simple:
for (int i = 0; i < listOfItems.ItemsIn.Count; ++i)
{
//CalculateItemsHeight(i);
ListBoxItem abc = (lb.ItemContainerGenerator.ContainerFromItem(lb.Items[i]) as ListBoxItem);
abc.UpdateLayout();
totalWidth += abc.ActualWidth;
}
Hopefully this helps!
The wpf layout engine won't have been through the layout and arrange pass so your listboxitems won't have been given a size yet. Sticking in the message box will allow the background threads that do this run. Try forcing a call to Measure() on your items before looking at their size.
SOLVED
This creates somewhat flickering effect for a fraction of second (as if loading items one by one), but actually suits my needs.
The point is to refresh the UI for an item before retrieving its height.
I have created an extension method:
public static void RefreshUI(this DependencyObject obj)
{
obj.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Loaded, (Action)delegate { });
}
And then before retrieving the height, I refresh the UI.
private double CalculateItemsHeight(int index)
{
ListBoxItem lbi = _box.ItemContainerGenerator.ContainerFromIndex(index) as ListBoxItem;
if (lbi != null) {
lbi.RefreshUI();
return lbi.ActualHeight;
}
return 0;
}