WPF accessing scrollviewer of a listview codebehind - c#

I need to access the scrollviewer of a listview from the codebehind.
here is the definition of my listview
<ListView Grid.Row="1" ItemsSource="{Binding Path=SpecList, UpdateSourceTrigger=PropertyChanged}"
Name="mylistview"
ItemTemplate="{StaticResource SpecElementTemplate}"
Background="{StaticResource EnvLayout}"
ScrollViewer.HorizontalScrollBarVisibility="Visible"
ScrollViewer.VerticalScrollBarVisibility="Disabled"
ItemContainerStyle="{StaticResource MyStyle}"
BorderBrush="Blue"
BorderThickness="20"
Margin="-2">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
How can I get the scrollviewer?
Thank you
Andrea

There are several ways to get the ScrollViewer. Simplest solution is to get the the first child of the first child of the ListView. This means get the Border and the ScrollViewer inside this Border like described in
this answer:
// Get the border of the listview (first child of a listview)
Decorator border = VisualTreeHelper.GetChild(mylistview, 0) as Decorator;
// Get scrollviewer
ScrollViewer scrollViewer = border.Child as ScrollViewer;
A second way is to scan all childrens recursive to find the ScrollViewer. This is described in the answer by Matt Hamilton in this question. You can simply use this function to get the ScrollViewer.
ScrollViewer scrollViewer = GetChildOfType<ScrollViewer>(mylistview);
This second solution is much more generic and will also work if the template of your ListView was edited.

Use VisualTreeHelper class to access any child control.
Psudeo code to your case:
//Declare a scroll viewer object.
ScrollViewer sViewer = default(ScrollViewer );
//Start looping the child controls of your listview.
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(YOUR_LISTVIEW_OBJECT.VisualParent ); i++)
{
// Retrieve child visual at specified index value.
Visual childVisual = (Visual)VisualTreeHelper.GetChild(YOUR_LISTVIEW_OBJECT.VisualParent , i);
ScrollViewer sViewer = childVisual as ScrollViewer;
//You got your scroll viewer. Stop looping.
if (sViewer != null)
{
break;
}
}

I also suggest using the CollectionChanged event. In this code, the CollectionChanged event handler is added to the codebehind after the view model has been loaded. Then, each time the collection changes we scroll to the bottom of the listview. Here is an important point. The scrollviewer child of the list view might not yet be completely rendered when our events start firing. Hence we will get exceptions if we try to use the VisualTreeHelper.GetChild method. So, we have to first attempt to get the scrollviewer and then ignore its positioning if it is not yet available.
private void ReceivedItems_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
// Make sure the items source property in the viewmodel has some items
if (myViewModel.ReceivedItems.Count > 0)
{
var aScrollViewer = RcvdListView.GetChildOfType<ScrollViewer>();
// Make sure the scrollviewer exists before trying to position it
if (aScrollViewer != null)
{
aScrollViewer.ScrollToBottom();
}
}
}

Listview's ScrollViewer should be accessible after LayoutUpdated. You could hook on LayoutUpdated and then get if from Visual tree
private static void ListView_LayoutUpdated(object sender, EventArgs e)
{
var listview = (ListView)sender;
var viewer = listview.GetFirstChildOfType<ScrollViewer>();
}
public static T GetFirstChildOfType<T>(this DependencyObject dependencyObject) where T : DependencyObject
{
if (dependencyObject == null)
{
return null;
}
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(dependencyObject); i++)
{
var child = VisualTreeHelper.GetChild(dependencyObject, i);
var result = (child as T) ?? GetFirstChildOfType<T>(child);
if (result != null)
{
return result;
}
}
return null;
}

Related

Datagrid sort, what happened to row?

I am coding a UserControl that can be use as a ScrollBar with color mark on it (like mostly all IDE).
My UserControl look
<UserControl x:Class="MarkedScrollBar" x:Name="Instance" ...>
<Grid Name="grd" >
<COLUMN/ROW DEF>...</>
<ScrollViewer Name="scrView" Grid.RowSpan="3" Grid.ColumnSpan="2" >
<my:IcuContentPresenter x:Name="presenter" Content="{Binding AdditionnalContent, ElementName=Instance}" ContentUpdated="presenter_ContentUpdated" />
</ScrollViewer>
<Canvas ...(Canvas place on the scrollbar) />
</Grid>
</UserControl>
On my UserControl, everything work fine. I can add content in my IcuContentPresenter (Extension of ContentPresenter to get 1 more event).
In my main window i use my MarkedScroolBar like this :
<my:IcuMarkedScrollBar Grid.Row="0" x:Name="bbb" Grid.Column="0" >
<my:IcuMarkedScrollBar.AdditionnalContent>
<my:IcuDataGrid ItemsSource="{Binding AAA, RelativeSource={RelativeSource AncestorType={x:Type Window}}}" Loaded="DataGrid_Loaded" Width="300" LoadingRow="DataGrid_LoadingRow" />
</my:IcuMarkedScrollBar.AdditionnalContent>
</my:IcuMarkedScrollBar>
Again, i have a DataGrid extension to get 1 event (Sorted event, code from here).
Now my problem is that when i sort my datagrid Inside my MarkedScrollBar, i want the mark to follow the content, that the mark is binding to.
First attempt, I try to link the mark with the control having a Dictionary Key=Mark, Value=Control. I was getting the DatagridRow that I wanted to have a mark for my datacontext.
First time the datagrid appear, my mark are all well place, but if I sort, my mark don't follow. I went into debugging and saw that there is a DataGridRow that contains my datacontext item, but it's not the one i save in my Dictionary.
End First attempt
Second attempt, with the DataGridRow changing when I sort, I thought that if I was linking my DataContext item to my mark it would fix the bug.
So now, when I sort my datagrid, I try to find the datagridrow that contains my datacontext item. But it does find anything.
But, when i reexecute the same code manually with a button in the window, it's work. All my mark appear at the good position.
End second attempt
Brief explanation of my extension :
DataGrid : I use it to get the Sorted event, because in Sorting event the row where not already move.
ContentPresenter : I add event on it to get modification to the content, but not to the Property (like datagrid sorting). For that i needed to check the type of the content, so i don't support everything for now.
My code for the ContentPresenter is that :
public class IcuContentPresenter : ContentPresenter
{
public event EventHandler<ControlEventArgs> ContentUpdated;
public IcuContentPresenter()
{
this.Loaded += OnLoaded;
}
protected void OnLoaded(object p_oSender, RoutedEventArgs p_oEvent)
{
Type t = this.Content.GetType();
if (t == typeof(IcuDataGrid))
{
IcuDataGrid oControl = this.Content as IcuDataGrid;
oControl.Sorted += CallEvent;
}
}
void CallEvent(object sender, EventArgs e)
{
if (ContentUpdated != null)
{
ContentUpdated(this, new ControlEventArgs(Content.GetType()));
}
}
public UIElement FindVisualElementForObject(object p_oObject)
{
UIElement oTheOne = null;
if (Content.GetType() == typeof(IcuDataGrid))
{
oTheOne = FindInDatagrid(p_oObject);
}
return oTheOne;
}
private UIElement FindInDatagrid(Object p_oObj)
{
DataGrid oGrid = Content as DataGrid;
var a = (DataGridRow)oGrid.ItemContainerGenerator.ContainerFromItem(p_oObj);
return a;
}
//----------------------------------------------------
// Inner class
//----------------------------------------------------
public class ControlEventArgs : EventArgs
{
public ControlEventArgs(Type value)
{
ControlType = value;
}
public Type ControlType { get; set; }
}
}
To support a control i will have to add them here. On the load of this control, I check the content, if the content is my Datagrid on the event Sorted i call the my Event ContentUpdated to be able to update my mark. But at this moment, it's like my datagrid contains no row with my datacontext item.
When virtualization is enabled, row elements will only be generated for data items which are actually in view. You will need to associate the marks with the underlying data items (row objects) rather than the DataGridRow elements. The Items property on the DataGrid should have the rows in their correctly sorted positions.

Hiding a sibling listbox when a textblock is clicked

I was wonder if any one knows how to change the visibility of a listbox within a DataTemplate when a sibling is clicked. The DataTemplate is being used on a listbox. The following is an example of the xaml I'm using:
<DataTemplate x:Key="Template1">
<StackPanel Margin="110,0,0,0">
<TextBlock Text="{Binding Name}" />
<TextBlock Name="ShowHide" Text="Hide" Tap="ShowHide_Tap" />
<ListBox Name="Listbox1" ItemsSource="{Binding SecondList}" Visibility="Visible" ItemTemplate="{StaticResource Template2}"/>
</StackPanel>
</DataTemplate>
The following is my attempt but I can't use the FindName
private void ShowHide_Click(object sender, System.Windows.Input.GestureEventArgs e)
{
var item = sender as TextBlock;
ListBox Listbox = null;
if (item != null)
{
ContentPresenter templateParent = GetFrameworkElementByName<ContentPresenter>(item);
DataTemplate dataTemplate = templateParent.ContentTemplate;
if (dataTemplate != null && templateParent != null)
{
Listbox = templateParent.FindName("Listbox1") as ListBox;
}
if (Listbox != null)
{
MessageBox.Show(String.Format("ERROR!"));
}
else
Listbox.Visibility = Visibility.Collapsed;
}
}
private static T GetFrameworkElementByName<T>(FrameworkElement referenceElement) where T : FrameworkElement
{
FrameworkElement child = null;
for (Int32 i = 0; i < VisualTreeHelper.GetChildrenCount(referenceElement); i++)
{
child = VisualTreeHelper.GetChild(referenceElement, i) as FrameworkElement;
System.Diagnostics.Debug.WriteLine(child);
if (child != null && child.GetType() == typeof(T))
{ break; }
else if (child != null)
{
child = GetFrameworkElementByName<T>(child);
if (child != null && child.GetType() == typeof(T))
{
break;
}
}
}
return child as T;
}
If any one has any insights they would be much appreciated,
Thanks.
It so happens that the Blend SDK provides functionality for this -- you can even use XAML only, no code behind. Just use an EventTrigger (on the "Tap" event) along with a ChangePropertyAction. Here's how it looks:
<TextBlock Name="ShowHide" Text="Hide" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="Tap">
<ei:ChangePropertyAction TargetName="Listbox1"
PropertyName="Visibility" Value="Collapsed" />
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBlock>
<ListBox Name="Listbox1" ItemsSource="{Binding SecondList}" Visibility="Visible" />
Note that this requires you to add references to the following Extensions:
System.Windows.Interactivity
Microsoft.Expression.Interactions
Reference them in XAML with the namespaces:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
Welcome to StackOverflow!
Generally speaking this is not the way to use WPF, especially if you're using DataTemplates. The purpose of the view in WPF is to display the view model and fire off user events, nothing more. By changing the Data Template at run-time you're effectively storing the state of the view inside the view itself. This runs totally against the grain of how WPF was designed to be used.
To do this properly your view model (i.e. the class with the SecondList property) should have an extra property called something like ListBoxVisibility and you would bind your listbox's Visibility member to that. An even cleaner method is to use a bool and then use a converter in the view to convert it from type bool to type Visibility. Either way the view model should also have a property of type ICommand (e.g. OnButtonPressedCommand) that the button invokes when the user presses it. The handler for OnButtonPressedCommand, which should also be in the view model, then sets ListBoxVisible or whatever to the value you want which then propagates through to the list box. Doing things this way keeps good separation of concerns and means the view model can be created and its visibility-changing behavior unit tested independently without having to create the view itself.

Listview inside of scrollviewer prevents scrollviewer scroll

I have a scrollviewer with a couple listboxes in it. The problem is if a user uses the middle mouse roller to scroll the scrollviewer while their mouse is over a listview. The listview scrolls its internal scrollviewer to the bottom and then continues to capture the mouse, preventing the containing scrollviewer from scrolling.
Any ideas on how to handle this?
That happens because the ListView's (ListBox's, actually) content template wraps its items with a ScrollViewer by itself.
The simplest way is to disable it by dropping your own Template for the inside ListView, one that doesn't create a ScrollViewer:
<ListView>
<ListView.Template>
<ControlTemplate>
<ItemsPresenter></ItemsPresenter>
</ControlTemplate>
</ListView.Template>
...
</ListView>
BTW the same happens if you have a ListView inside a ListView (this was my case).
IMO, the best way to handle this scenario is to create a custom control :
class MyScrollViewer : ScrollViewer
{
protected override void OnPreviewMouseWheel(MouseWheelEventArgs e)
{
base.OnPreviewMouseWheel(e);
if (!e.Handled)
{
e.Handled = true;
this.RaiseEvent(new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
{
RoutedEvent = UIElement.MouseWheelEvent,
Source = this
});
}
}
}
Did you try disabling the ListView's ScrollBars?
<ListView ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Disabled" />
Inspired by some helpful answers, I have an implementation that scrolls ancestor ScrollViewers when inner ones (including from ListView, ListBox, DataGrid) scroll past their top/bottom.
I apply an attached property to all ScrollViewers in App.xaml:
<Style TargetType="ScrollViewer" BasedOn="{StaticResource {x:Type ScrollViewer}}">
<Setter Property="local:ScrollViewerHelper.FixMouseWheel" Value="True" />
</Style>
The attached property detects scrolling past top/bottom, and when that happens raises a mouse wheel event on the ScrollViewer's parent. Event routing gets it to the outer ScrollViewer:
public static class ScrollViewerHelper
{
// Attached property boilerplate
public static bool GetFixMouseWheel(ScrollViewer scrollViewer) => (bool)scrollViewer?.GetValue(FixMouseWheelProperty);
public static void SetFixMouseWheel(ScrollViewer scrollViewer, bool value) => scrollViewer?.SetValue(FixMouseWheelProperty, value);
public static readonly DependencyProperty FixMouseWheelProperty =
DependencyProperty.RegisterAttached("FixMouseWheel", typeof(bool), typeof(ScrollViewerHelper),
new PropertyMetadata(OnFixMouseWheelChanged));
// End attached property boilerplate
static void OnFixMouseWheelChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var scrollViewer = d as ScrollViewer;
if (scrollViewer == null) return;
scrollViewer.PreviewMouseWheel += (s2, e2) =>
{
var parent = scrollViewer.Parent as UIElement;
bool hitTopOrBottom = HitTopOrBottom(e2.Delta, scrollViewer);
if (parent is null || !hitTopOrBottom) return;
var argsCopy = Copy(e2);
parent.RaiseEvent(argsCopy);
};
}
static bool HitTopOrBottom(double delta, ScrollViewer scrollViewer)
{
var contentVerticalOffset = scrollViewer.ContentVerticalOffset;
var atTop = contentVerticalOffset == 0;
var movedUp = delta > 0;
var hitTop = atTop && movedUp;
var atBottom =
contentVerticalOffset == scrollViewer.ScrollableHeight;
var movedDown = delta < 0;
var hitBottom = atBottom && movedDown;
return hitTop || hitBottom;
}
static MouseWheelEventArgs Copy(MouseWheelEventArgs e)
=> new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
{
RoutedEvent = UIElement.MouseWheelEvent,
Source = e.Source,
};
}
If you wrap the inner listview in a scrollviewer then the scrolling will work.
<ListView ScrollViewer.VerticalScrollBarVisibility="Auto" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListView.ItemTemplate>
<DataTemplate>
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
<ListView>
<ListView.ItemTemplate>
<DataTemplate>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ScrollViewer>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>

ListBox ScrollIntoView when using CollectionViewSource with GroupDescriptions (i.e. IsGrouping == True)

Short version
I would like to scroll the ListBox item into view when the selection is changed.
Long version
I have a ListBox with the ItemsSource bound to a CollectionViewSource with a GroupDescription, as per the example below.
<Window.Resources>
<CollectionViewSource x:Key="AnimalsView" Source="{Binding Source={StaticResource Animals}, Path=AnimalList}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Category"/>
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</Window.Resources>
<ListBox x:Name="AnimalsListBox"ItemsSource="{Binding Source={StaticResource AnimalsView}}" ItemTemplate="{StaticResource AnimalTemplate}" SelectionChanged="ListBox_SelectionChanged">
<ListBox.GroupStyle>
<GroupStyle HeaderTemplate="{StaticResource CategoryTemplate}" />
</ListBox.GroupStyle>
</ListBox>
There is a SelectionChanged event in the a code-behind file.
public List<Animal> Animals { get; set; }
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ListBox control = (ListBox)sender;
control.ScrollIntoView(control.SelectedItem);
}
Now. If I set the AnimalsListBox.SelectedItem to an item that is currently not visible I would like to have it scroll in view. This is where it gets tricky, as the ListBox is being groups (the IsGrouped property is true) the call to ScrollIntoView fails.
System.Windows.Controls.ListBox via Reflector. Note the base.IsGrouping in the OnBringItemIntoView.
public void ScrollIntoView(object item)
{
if (base.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
this.OnBringItemIntoView(item);
}
else
{
base.Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new DispatcherOperationCallback(this.OnBringItemIntoView), item);
}
}
private object OnBringItemIntoView(object arg)
{
FrameworkElement element = base.ItemContainerGenerator.ContainerFromItem(arg) as FrameworkElement;
if (element != null)
{
element.BringIntoView();
}
else if (!base.IsGrouping && base.Items.Contains(arg))
{
VirtualizingPanel itemsHost = base.ItemsHost as VirtualizingPanel;
if (itemsHost != null)
{
itemsHost.BringIndexIntoView(base.Items.IndexOf(arg));
}
}
return null;
}
Questions
Can anyone explain why it does not work when using grouping?
The ItemContainerGenerator.ContainerFromItem always returns null, even though it's status states that all the containers have been generated.
How I can achieve the scrolling into view when using grouping?
I have found a solution to my problem. I was certain that I wasn't the first person to hit this issue so I continued to search StackOverflow for solutions and I stumbled upon this answer by David about how ItemContainerGenerator works with a grouped list.
David's solution was to delay accessing the ItemContainerGenerator until after the rendering process.
I have implemented this solution, with a few changes that I will detail after.
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ListBox control = (ListBox)sender;
if (control.IsGrouping)
{
if (control.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(DelayedBringIntoView));
else
control.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
}
else
control.ScrollIntoView(control.SelectedItem);
}
private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
if (ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
return;
ItemContainerGenerator.StatusChanged -= ItemContainerGenerator_StatusChanged;
Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(DelayedBringIntoView));
}
private void DelayedBringIntoView()
{
var item = ItemContainerGenerator.ContainerFromItem(SelectedItem) as ListBoxItem;
if (item != null)
item.BringIntoView();
}
Changes:
Only uses the ItemContainerGenerator approach when it IsGrouping is true, otherwise continue to use the default ScrollIntoView.
Check if the ItemContainerGenerator is ready, if so dispatch the action, otherwise listen for the ItemContainerGenerator status to change.. This is important as if it is ready then the StatusChanged event will never fire.
The out of the box VirtualizingStackPanel does not support virtualizing grouped collection views. When a grouped collection is rendered in an ItemsControl, each group as a whole is an item as opposed to each item in the collection which results in "jerky" scrolling to each group header and not each item.
You'll probably need to roll your own VirtualizingStackPanel or ItemContainerGenerator in order to keep track of the containers displayed in a group. It sounds ridiculous, but the default virtualization with grouping in WPF is lacking to say the least.

How to get next sibling of the item in databound items control?

How to get next sibling of the element in visual tree? This elment is dataitem of the databound ItemsSource. My goal is get acces to sibling in code (assume that i have access to the element itself) and then use BringIntoView.
Thanks.
For example, if your ItemsControl is a ListBox, the elements will be ListBoxItem objects. If you have one ListBoxItem and you want the next ListBoxItem in the list, you can use the ItemContainerGenerator API to find it like this:
public static DependencyObject GetNextSibling(ItemsControl itemsControl, DependencyObject sibling)
{
var n = itemsControl.Items.Count;
var foundSibling = false;
for (int i = 0; i < n; i++)
{
var child = itemsControl.ItemContainerGenerator.ContainerFromIndex(i);
if (foundSibling)
return child;
if (child == sibling)
foundSibling = true;
}
return null;
}
Here's some sample XAML:
<Grid>
<ListBox Name="listBox">
<ListBoxItem Name="item1">Item1</ListBoxItem>
<ListBoxItem Name="item2">Item2</ListBoxItem>
</ListBox>
</Grid>
and the code-behind:
void Window_Loaded(object sender, RoutedEventArgs e)
{
var itemsControl = listBox;
var sibling = item1;
var nextSibling = GetNextSibling(itemsControl, sibling) as ListBoxItem;
MessageBox.Show(string.Format("Sibling is {0}", nextSibling.Content));
}
which results in:
This also works if the ItemsControl is data-bound. If you only have the data item (not the corresponding user interface element), you can use the ItemContainerGenerator.ContainerFromItem API to get the initial sibling.

Categories