ScrollIntoView - item should not disapear from view
From the start I wanna say I will award a 200 Bounty to somebody who can help me with my problem.
This is my simple code(C# with WPF):
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
///
public partial class Window1 : Window
{
string fixedItem;
public Window1()
{
InitializeComponent();
listBox1.ItemContainerGenerator.ItemsChanged += new System.Windows.Controls.Primitives.ItemsChangedEventHandler(list_changes);
}
private void list_changes(object sender, System.Windows.Controls.Primitives.ItemsChangedEventArgs e)
{
listBox1.ScrollIntoView(fixedItem);
}
private void listBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
fixedItem = (string)listBox1.SelectedItem;
}
private void button1_Click(object sender, RoutedEventArgs e)
{
listBox1.Items.Add("item0");
listBox1.Items.Add("item1");
listBox1.Items.Add("item2");
listBox1.Items.Add("item3");
listBox1.Items.Add("item4");
listBox1.Items.Add("item5");
listBox1.Items.Add("item6");
}
private void button2_Click(object sender, RoutedEventArgs e)
{
listBox1.Items.Insert(0, "item7");
listBox1.Items.Insert(0, "item8");
listBox1.Items.Insert(0, "item9");
listBox1.Items.Insert(0, "item10");
listBox1.Items.Insert(0, "item11");
listBox1.Items.Insert(0, "item12");
listBox1.Items.Insert(0, "item13");
listBox1.Items.Insert(0, "item14");
listBox1.Items.Insert(0, "item15");
}
private void button3_Click(object sender, RoutedEventArgs e)
{
listBox1.Items.Insert(0, "item16");
listBox1.Items.Insert(0, "item17");
listBox1.Items.Insert(0, "item18");
listBox1.Items.Insert(0, "item19");
listBox1.Items.Insert(0, "item20");
listBox1.Items.Insert(0, "item21");
listBox1.Items.Insert(0, "item22");
listBox1.Items.Insert(0, "item23");
listBox1.Items.Insert(0, "item24");
}
}
}
First I just made 3 Buttons to insert some text in the listBox. Lets say I click button1 and button2, I will have this list:
item15
item14
item13
item12
......
item7
item0
item1
.....
item6
After that I want to click on "item12", then when I click button3 I want my "item12" to remain in the same spot while the text is generated(4th position in the list).
In short words everytime I click on item I want it to remain in the exctly same position while generating text.So anyone has any idee how to do this ? Do I need to use ScrollViewer object to work with VerticallOffset and ViewportHeigth ? This simple code that I posted when I click on the item and then generate text it will move the item at the bottom(viewable position) and remains there after. But I don开发者_StackOverflow't want to move it at all.
Edit:
Ok I tried this code from here:
FrameworkElement container = listRadioItems.ItemContainerGenerator.ContainerFromItem(fixedItem) as FrameworkElement;
if (null != container)
{
if (ScrollViewer.GetCanContentScroll(listBox))
{
IScrollInfo scrollInfo = VisualTreeHelper.GetParent(container) as IScrollInfo;
if (null != scrollInfo)
{
StackPanel stackPanel = scrollInfo as StackPanel;
VirtualizingStackPanel virtualizingStackpanel = scrollInfo as VirtualizingStackPanel;
int index = listBox.ItemContainerGenerator.IndexFromContainer(container);
if (((null != stackPanel) && (Orientation.Horizontal == stackPanel.Orientation)) || ((null != virtualizingStackpanel) && (Orientation.Horizontal == virtualizingStackpanel.Orientation)))
{
scrollInfo.SetHorizontalOffset(index - Math.Floor(scrollInfo.ViewportWidth / 2));
}
else
{
scrollInfo.SetVerticalOffset(index - Math.Floor(scrollInfo.ViewportHeight / 2));
}
}
}
else
{
Rect rect = new Rect(new Point(), container.RenderSize);
FrameworkElement constrainingParent = container;
do
{
constrainingParent = VisualTreeHelper.GetParent(constrainingParent) as FrameworkElement;
} while ((null != constrainingParent) && (listBox != constrainingParent) && !(constrainingParent is ScrollContentPresenter));
if (null != constrainingParent)
{
rect.Inflate(Math.Max((constrainingParent.ActualWidth - rect.Width) / 2, 0), Math.Max((constrainingParent.ActualHeight - rect.Height) / 2, 0));
}
container.BringIntoView(rect);
}
}
What it does for me is it centeres the item selected, but the scroll goes down, only sometimes it also centeres. And my problem is that again, only sometimes the selected item will disapear from view.
What would be fantastic is if I can make both the item and the scroll centered. But first concern is the item should not disapear from view.
Here's some code that gets the first visible item in your ListBox, adds the items to whatever index you specify, then uses the Dispatcher to scroll the list back to the first visible item after all the new items have been rendered.
The ScrollIntoViewTop()
extension method is the same method as the link you posted in your original question, but I altered it to leave the item at the top of the list.
The WPFHelpers.IsObjectVisibleInContainer()
method is one I've used in the past to test if an object is fully or partially visible within another container. To get the first visible item, I simply loop through the ListBox items, get the ListBoxItem container associated with each item, then check if that container is visible or not. The first one that returns true is the first visible item in the ListBox.
Here's the full code:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void button1_Click(object sender, RoutedEventArgs e)
{
var firstVisibleItem = GetFirstVisibleItem(listBox1);
listBox1.Items.Insert(0, "item0");
listBox1.Items.Insert(0, "item1");
listBox1.Items.Insert(0, "item2");
listBox1.Items.Insert(0, "item3");
listBox1.Items.Insert(0, "item4");
listBox1.Items.Insert(0, "item5");
listBox1.Items.Insert(0, "item6");
if (firstVisibleItem != null)
{
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Loaded,
new Action(delegate()
{
listBox1.ScrollIntoViewTop(firstVisibleItem);
}));
}
}
private void button2_Click(object sender, RoutedEventArgs e)
{
var firstVisibleItem = GetFirstVisibleItem(listBox1);
listBox1.Items.Insert(0, "item7");
listBox1.Items.Insert(0, "item8");
listBox1.Items.Insert(0, "item9");
listBox1.Items.Insert(0, "item10");
listBox1.Items.Insert(0, "item11");
listBox1.Items.Insert(0, "item12");
listBox1.Items.Insert(0, "item13");
listBox1.Items.Insert(0, "item14");
listBox1.Items.Insert(0, "item15");
if (firstVisibleItem != null)
{
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Loaded,
new Action(delegate()
{
listBox1.ScrollIntoViewTop(firstVisibleItem);
}));
}
}
private void button3_Click(object sender, RoutedEventArgs e)
{
var firstVisibleItem = GetFirstVisibleItem(listBox1);
listBox1.Items.Insert(0, "item16");
listBox1.Items.Insert(0, "item17");
listBox1.Items.Insert(0, "item18");
listBox1.Items.Insert(0, "item19");
listBox1.Items.Insert(0, "item20");
listBox1.Items.Insert(0, "item21");
listBox1.Items.Insert(0, "item22");
listBox1.Items.Insert(0, "item23");
listBox1.Items.Insert(0, "item24");
if (firstVisibleItem != null)
{
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Loaded,
new Action(delegate()
{
listBox1.ScrollIntoViewTop(firstVisibleItem);
}));
}
}
private object GetFirstVisibleItem(ListBox listBox)
{
foreach (var item in listBox.Items)
{
var itemContainer = (ListBoxItem)listBox.ItemContainerGenerator.ContainerFromItem(item);
if (WPFHelpers.IsObjectVisibleInContainer(itemContainer, listBox) == ControlVisibility.Full)
{
return item;
}
}
return null;
}
}
public enum ControlVisibility
{
Hidden,
Partial,
Full,
FullHeightPartialWidth,
FullWidthPartialHeight
}
public class WPFHelpers
{
/// <summary>
/// Checks to see if an object is rendered visible within a parent container
/// </summary>
/// <param name="child">UI element of child object</param>
/// <param name="parent">UI Element of parent object</param>
/// <returns>ControlVisibility Enum: Hidden, Partial or Visible</returns>
public static ControlVisibility IsObjectVisibleInContainer(FrameworkElement child, UIElement parent)
{
GeneralTransform childTransform = child.TransformToAncestor(parent);
//Rect childSize = childTransform.TransformBounds(new Rect(new Point(0, 0), child.RenderSize));
Rect childSize = childTransform.TransformBounds(new Rect(new Point(0, 0), new Point(child.ActualWidth, child.ActualHeight)));
Rect result = Rect.Intersect(new Rect(new Point(0, 0), parent.RenderSize), childSize);
if (result == Rect.Empty)
{
return ControlVisibility.Hidden;
}
if (result.Height == childSize.Height && result.Width == childSize.Width)
{
return ControlVisibility.Full;
}
if (result.Height == childSize.Height)
{
return ControlVisibility.FullHeightPartialWidth;
}
if (result.Width == childSize.Width)
{
return ControlVisibility.FullWidthPartialHeight;
}
return ControlVisibility.Partial;
}
}
/// <summary>
/// Class implementing helpful extensions to ListBox.
/// </summary>
public static class ListBoxExtensions
{
/// <summary>
/// Causes the object to scroll into view centered.
/// </summary>
/// <param name="listBox">ListBox instance.</param>
/// <param name="item">Object to scroll.</param>
//[SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters",
// Justification = "Deliberately targeting ListBox.")]
public static void ScrollIntoViewTop(this ListBox listBox, object item)
{
Debug.Assert(!VirtualizingStackPanel.GetIsVirtualizing(listBox),
"VirtualizingStackPanel.IsVirtualizing must be disabled for ScrollIntoViewCentered to work.");
// Get the container for the specified item
var container = listBox.ItemContainerGenerator.ContainerFromItem(item) as FrameworkElement;
if (null != container)
{
if (ScrollViewer.GetCanContentScroll(listBox))
{
// Get the parent IScrollInfo
var scrollInfo = VisualTreeHelper.GetParent(container) as IScrollInfo;
if (null != scrollInfo)
{
// Need to know orientation, so parent must be a known type
var stackPanel = scrollInfo as StackPanel;
var virtualizingStackPanel = scrollInfo as VirtualizingStackPanel;
Debug.Assert((null != stackPanel) || (null != virtualizingStackPanel),
"ItemsPanel must be a StackPanel or VirtualizingStackPanel for ScrollIntoViewCentered to work.");
// Get the container's index
var index = listBox.ItemContainerGenerator.IndexFromContainer(container);
// Center the item by splitting the extra space
if (((null != stackPanel) && (Orientation.Horizontal == stackPanel.Orientation)) ||
((null != virtualizingStackPanel) && (Orientation.Horizontal == virtualizingStackPanel.Orientation)))
{
//scrollInfo.SetHorizontalOffset(index - Math.Floor(scrollInfo.ViewportWidth / 2));
scrollInfo.SetHorizontalOffset(index);
}
else
{
//scrollInfo.SetVerticalOffset(index - Math.Floor(scrollInfo.ViewportHeight / 2));
scrollInfo.SetVerticalOffset(index);
}
}
}
else
{
// Get the bounds of the item container
var rect = new Rect(new Point(), container.RenderSize);
// Find constraining parent (either the nearest ScrollContentPresenter or the ListBox itself)
FrameworkElement constrainingParent = container;
do
{
constrainingParent = VisualTreeHelper.GetParent(constrainingParent) as FrameworkElement;
} while ((null != constrainingParent) &&
(listBox != constrainingParent) &&
!(constrainingParent is ScrollContentPresenter));
if (null != constrainingParent)
{
// Inflate rect to fill the constraining parent
rect.Inflate(
Math.Max((constrainingParent.ActualWidth - rect.Width) / 2, 0),
Math.Max((constrainingParent.ActualHeight - rect.Height) / 2, 0));
}
// Bring the (inflated) bounds into view
container.BringIntoView(rect);
}
}
}
}
I made few changes to your code, please have a look.
Before adding items on button 2 and button 3 click... i am getting fixeditem and fixed item index and rearranging the fixed item after adding items....
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
string fixedItem;
public Window1()
{
InitializeComponent();
listBox1.ItemContainerGenerator.ItemsChanged += new System.Windows.Controls.Primitives.ItemsChangedEventHandler(list_changes);
}
private void list_changes(object sender, System.Windows.Controls.Primitives.ItemsChangedEventArgs e)
{
listBox1.ScrollIntoView(fixedItem);
}
private void listBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
//fixedItem = (string)listBox1.SelectedItem;
}
private void button1_Click(object sender, RoutedEventArgs e)
{
listBox1.Items.Add("item0");
listBox1.Items.Add("item1");
listBox1.Items.Add("item2");
listBox1.Items.Add("item3");
listBox1.Items.Add("item4");
listBox1.Items.Add("item5");
listBox1.Items.Add("item6");
}
private void button2_Click(object sender, RoutedEventArgs e)
{
fixedItem = (string)listBox1.SelectedItem;
int selectedIndex = listBox1.SelectedIndex;
listBox1.Items.Insert(0, "item7");
listBox1.Items.Insert(0, "item8");
listBox1.Items.Insert(0, "item9");
listBox1.Items.Insert(0, "item10");
listBox1.Items.Insert(0, "item11");
listBox1.Items.Insert(0, "item12");
listBox1.Items.Insert(0, "item13");
listBox1.Items.Insert(0, "item14");
listBox1.Items.Insert(0, "item15");
listBox1.Items.Remove(fixedItem);
listBox1.Items.Insert(selectedIndex, fixedItem);
listBox1.SelectedItem = fixedItem;
}
private void button3_Click(object sender, RoutedEventArgs e)
{
fixedItem = (string)listBox1.SelectedItem;
int selectedIndex = listBox1.SelectedIndex;
listBox1.Items.Insert(0, "item16");
listBox1.Items.Insert(0, "item17");
listBox1.Items.Insert(0, "item18");
listBox1.Items.Insert(0, "item19");
listBox1.Items.Insert(0, "item20");
listBox1.Items.Insert(0, "item21");
listBox1.Items.Insert(0, "item22");
listBox1.Items.Insert(0, "item23");
listBox1.Items.Insert(0, "item24");
listBox1.Items.Remove(fixedItem);
listBox1.Items.Insert(selectedIndex, fixedItem);
listBox1.SelectedItem = fixedItem;
}
}
}
Update with Reference Type: I created a class called item.. instead of adding string i am adding item...
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
Item fixedItem;
int selectedIndex;
public Window1()
{
InitializeComponent();
listBox1.ItemContainerGenerator.ItemsChanged += new System.Windows.Controls.Primitives.ItemsChangedEventHandler(list_changes);
}
private void list_changes(object sender, System.Windows.Controls.Primitives.ItemsChangedEventArgs e)
{
}
private void listBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
}
private void button1_Click(object sender, RoutedEventArgs e)
{
listBox1.Items.Add(new Item { ItemName = "item0" });
listBox1.Items.Add(new Item { ItemName = "item1" });
listBox1.Items.Add(new Item { ItemName = "item2" });
listBox1.Items.Add(new Item { ItemName = "item3" });
listBox1.Items.Add(new Item { ItemName = "item4" });
listBox1.Items.Add(new Item { ItemName = "item5" });
listBox1.Items.Add(new Item { ItemName = "item6" });
}
private void button2_Click(object sender, RoutedEventArgs e)
{
if (listBox1.SelectedItem != null)
{
fixedItem = (Item)listBox1.SelectedItem;
selectedIndex = listBox1.SelectedIndex;
}
listBox1.Items.Insert(0, new Item { ItemName = "item7" });
listBox1.Items.Insert(0, new Item { ItemName = "item8" });
listBox1.Items.Insert(0, new Item { ItemName = "item9" });
listBox1.Items.Insert(0, new Item { ItemName = "item10" });
listBox1.Items.Insert(0, new Item { ItemName = "item11" });
listBox1.Items.Insert(0, new Item { ItemName = "item12" });
listBox1.Items.Insert(0, new Item { ItemName = "item13" });
listBox1.Items.Insert(0, new Item { ItemName = "item14" });
listBox1.Items.Insert(0, new Item { ItemName = "item15" });
listBox1.Items.Remove(fixedItem);
listBox1.Items.Insert(selectedIndex, fixedItem);
listBox1.SelectedItem = fixedItem;
listBox1.ScrollIntoView(fixedItem);
}
private void button3_Click(object sender, RoutedEventArgs e)
{
if (listBox1.SelectedItem != null)
{
fixedItem = (Item)listBox1.SelectedItem;
selectedIndex = listBox1.SelectedIndex;
}
listBox1.Items.Insert(0, new Item { ItemName = "item16" });
listBox1.Items.Insert(0, new Item { ItemName = "item17" });
listBox1.Items.Insert(0, new Item { ItemName = "item18" });
listBox1.Items.Insert(0, new Item { ItemName = "item19" });
listBox1.Items.Insert(0, new Item { ItemName = "item20" });
listBox1.Items.Insert(0, new Item { ItemName = "item21" });
listBox1.Items.Insert(0, new Item { ItemName = "item22" });
listBox1.Items.Insert(0, new Item { ItemName = "item23" });
listBox1.Items.Insert(0, new Item { ItemName = "item24" });
listBox1.Items.Remove(fixedItem);
listBox1.Items.Insert(selectedIndex, fixedItem);
listBox1.SelectedItem = fixedItem;
listBox1.ScrollIntoView(fixedItem);
}
}
class Item
{
public string ItemName { get; set; }
}
}
XAML Change....
<ListBox x:Name="listBox1" Height="300" SelectionChanged="listBox1_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ItemName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
do you want to select "item12" - then add more items - and after this "item12" should still be selected and in view?
if yes why not use ICollectionView.MoveTo method after adding new items? you just have to remember the last selected items before adding new items.
i use MoveTo and ScrollIntoView in my projects, it works fine.
EDIT:
i do not add items directly to the listbox. i use a collection and add this collection to the itemssource of the listbox. i use the SelectionChanged event for ScrollItemsIntoView.
var _myview = (ICollectionView)CollectionViewSource.GetDefaultView(this._mycollection);
_myview.MoveCurrentTo(this.rememberredItem);
if you work with Ado.net collections then you have to take BindingListCollectionView instead of ICollectionView
EDIT2: instead of CollectionViewSource.GetDefaultView(listBox1.ItemsSource) you could try the following. create a observablecollection and set the collection as itemssource for your listbox.
ICollectionView myview;
OberservableCollection<string> mysource = new ObservableCollection<string>();
myview = (ICollectionView)CollectionViewSource.GetDefaultView(this.mysource);
listbox1.ItemsSource = mysource; //you should better use binding in xaml here
if you want to add items now just add it to the collection.
this.mysource.Add("Item 13");
this.mysource.Add("Item 14");
EDIT3: copied from here
David Anson posted some articles on his blog that might help you here: Part 1 and Part 2. He gives an extension method that centers an item in an List Box. You might be able to build on that
Below code is working just hold the postion untill you menual changes the selected item.
XAML Code
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="189*" VirtualizingStackPanel.IsVirtualizing="False" VirtualizingStackPanel.VirtualizationMode="Recycling"/>
<RowDefinition Height="72*" />
</Grid.RowDefinitions>
<ListBox x:Name="listBox1" Grid.Row="0" />
<Button Click="Button_Click" Grid.Row="1" />
</Grid>
C# code
private void Button_Click(object sender, RoutedEventArgs e)
{
if (listBox1.Items.Count <= 0)
{
for (int i = 0; i < 25; i++)
{
ListBoxItem item = new ListBoxItem();
item.Content = "Content " + i;
listBox1.Items.Insert(i, item);
// listBox1.SelectedItem = item;
}
listBox1.SelectedItem = listBox1.Items[12];
}
else
{
ListBoxItem item = new ListBoxItem();
item.Content = "Content " + listBox1.Items.Count;
listBox1.Items.Insert(listBox1.Items.Count, item);
// listBox1.SelectedItem = item;
}
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Loaded,
new Action(delegate()
{
listBox1.ScrollIntoView(listBox1.SelectedItem);
}));
}
You could try this:
namespace WpfApplication1 {
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
///
public partial class Window1 : Window
{
string fixedItem;
public Window1()
{
InitializeComponent();
listBox1.ItemContainerGenerator.ItemsChanged += new System.Windows.Controls.Primitives.ItemsChangedEventHandler(list_changes);
}
private void list_changes(object sender, System.Windows.Controls.Primitives.ItemsChangedEventArgs e)
{
listBox1.UpdateLayout();
listBox1.ScrollIntoView(fixedItem);
}
// REST OF YOUR CODE...
UPDATE If you meant your scrolling is right already, only moving the item to the last viewable position, I'd get the item from three (or more) indexes ahead in order to centralize the item on the listbox.
i've tested this in Silverlight but the difference is there
your code doesn't works
private void list_changes(object sender, System.Windows.Controls.Primitives.ItemsChangedEventArgs e)
{
listBox1.ScrollIntoView(fixedItem);
}
but this code works !
private void list_changes(object sender, System.Windows.Controls.Primitives.ItemsChangedEventArgs e)
{
Dispatcher.BeginInvoke(() => listBox1.ScrollIntoView(fixedItem));
}
the problems comes from the tact that the listbox scrolls and then adds the new item so then REscroll.. if you call the scroll on the dispatcher, the listbox will finish its work adding items and THEN scroll to your item
精彩评论