Reputation: 470
I build a user control that should show a list of status indicators at the bottom of our application. I use an ItemsControl
to create the list and enabled it to scroll horizontally via mouse wheel but also added two buttons (Left and Right) to replace the usual scroll bar (because the scrollbar would conflict with the look and feel and would be to big in this situation).
If there are not enough Items in the List or the control is wide enough to show all items, I want to collapse the buttons and of course show them again as soon as scrolling is needed.
Currently I use the ScrollViewer
's SizeChanged
event to detect if the width has changed and if any items of the ItemsControl
are not visible so it needs scrolling. (See C# Code at the bottom of the post.)
My Issue is that it works fine as long as I change the size by gabbing the edge of the window with the mouse and resize it that way but it does not work as soon as the window size is programmaticaly changed or by double clicking on the window title to make it full screen (or use the maximize/minimize button).
This is my UserControl:
<Grid x:Name="grid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<dx:SimpleButton x:Name="scroll_left_button" Grid.Column="0" Content="←"/>
<Border Background="#FF00ACDC" Grid.Column="1" BorderThickness="0">
<ItemsControl x:Name="items_control" w3ClientUi:ScrollViewerHelper.UseHorizontalScrolling="True"
ScrollViewer.HorizontalScrollBarVisibility="Hidden" ScrollViewer.VerticalScrollBarVisibility="Disabled"
ScrollViewer.CanContentScroll="True" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Template>
<ControlTemplate TargetType="ItemsControl">
<ScrollViewer HorizontalScrollBarVisibility="Hidden">
<ItemsPresenter/>
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<!-- dummy template -->
<StackPanel.ToolTip>
<TextBlock Text="{Binding Name}"/>
</StackPanel.ToolTip>
<panda:PndLabel Padding="0 10 2 10" Content="{Binding Number}" FontSize="16" FontWeight="Normal"
Foreground="White" Margin="0 0 2 0"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
<!-- ItemsSource is set in Code -->
</ItemsControl>
</Border>
<dx:SimpleButton Grid.Column="2" x:Name="scroll_right_button" Content="→"/>
</Grid>
In Code behind I get the ScrollViewer
instance from the ItemsControl
as soon as it's loaded and then attach to the SizeChanged
event.
When changing the window size via the window title or programmaticaly the event is thrown but the ScrollableWidth
is not updated yet and therefore my buttons are still visible in full screen (or still collapsed when it gets smaller).
scroll_viewer.SizeChanged += (s, e) =>
{
if (!e.WidthChanged) return;
if (scroll_viewer?.ScrollableWidth > 0)
{
scroll_left_button.Visibility = Visibility.Visible;
scroll_right_button.Visibility = Visibility.Visible;
}
else
{
scroll_left_button.Visibility = Visibility.Collapsed;
scroll_right_button.Visibility = Visibility.Collapsed;
}
};
Upvotes: 2
Views: 1154
Reputation: 470
So, after coming back from the weekend with a fresh mind I figured that, in my case, handling the SizeChanged
event is the wrong approach since all I actually need to know is if there are any items of my ItemsControl
that need scrolling and if the number has changed.
In my question I already checked the ScrollViewer
's ScrollableWidth
property. It shows how many items are not visible and need scrolling, so it would be enough to check if it has changed and if it's new Value is greater than zero.
ScrollableWidth
is a DependencyProperty
so I thought about binding to it and listening to the changed event.
So what I did first is creating the new DependencyProperty
on my UserControl
// Dependency Property is Private since I only need it internally
private static readonly DependencyProperty scrollable_width_property =
DependencyProperty.Register(nameof(scrollable_width), typeof(double),
typeof(MyUserControl),
new FrameworkPropertyMetadata(null) { BindsTwoWayByDefault = false, PropertyChangedCallback = property_changed_callback });
// Wrapper only has get because ScrollableWidth is read only anyway
private double scrollable_width => (double)GetValue(scrollable_width_property);
// Listening to the change
private static void property_changed_callback(DependencyObject dpo, DependencyPropertyChangedEventArgs args)
{
var o = dpo as MyUserControl;
o?.SetButtonVisibility((double)args.NewValue > 0);
}
I removed the scroll_viewer.SizeChanged
event and instead created a new public method on my user control to change the button visibility.
public void SetButtonVisibility(bool visible)
{
if (scroll_viewer == null) return;
if (visible)
{
scroll_left_button.Visibility = Visibility.Visible;
scroll_right_button.Visibility = Visibility.Visible;
}
else
{
scroll_left_button.Visibility = Visibility.Collapsed;
scroll_right_button.Visibility = Visibility.Collapsed;
}
}
The last thing that has to be done is the actual binding.
I do it once, in the ItemsControl
's Loaded
event after obtaining the actual ScrollViewer
.
var binding = new Binding(nameof(ScrollViewer.ScrollableWidth))
{
Source = scroll_viewer
};
SetBinding(scrollable_width_property, binding);
Now, whenever the ItemControl
needs scrolling (or doesn't), the visibility of the buttons will change regardless of the size change. And now it also works when using the maximize/minimize buttons in the window title.
It's probably not the best implementation since it could probably better use style trigger in xaml instead of the SetButtonVisibility
method but it gets the job done.
Edit:
In my case I also had to add a SetButtonVisibility(scroll_viewer.ScrollableWidth > 0);
to the ItemsContorol
's LoadedEvent
because the callback is not triggered at startup.
Upvotes: 3