WPF UserControl Design Time Size - wpf

When creating a UserControl in WPF, I find it convenient to give it some arbitrary Height and Width values so that I can view my changes in the Visual Studio designer. When I run the control, however, I want the Height and Width to be undefined, so that the control will expand to fill whatever container I place it in. How can I acheive this same functionality without having to remove the Height and Width values before building my control? (Or without using DockPanel in the parent container.)
The following code demonstrates the problem:
<Window x:Class="ExampleApplication3.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:ExampleApplication3"
Title="Example" Height="600" Width="600">
<Grid Background="LightGray">
<loc:UserControl1 />
</Grid>
</Window>
The following definition of UserControl1 displays reasonably at design time but displays as a fixed size at run time:
<UserControl x:Class="ExampleApplication3.UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="300" Width="300">
<Grid Background="LightCyan" />
</UserControl>
The following definition of UserControl1 displays as a dot at design time but expands to fill the parent Window1 at run time:
<UserControl x:Class="ExampleApplication3.UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid Background="LightCyan" />
</UserControl>

For Blend, a little known trick is to add these attributes to your usercontrol or window:
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="500" d:DesignWidth="600"
This will set the design height and width to 500 and 600 respectively. However this will only work for the blend designer. Not the Visual Studio Designer.
As far as the Visual Studio Designer your technique is all that works. Which is why I don't use the Visual Studio Designer. ;)

In Visual Studio add the Width and Height attribute to your UserControl XAML, but in the code-behind insert this
public UserControl1()
{
InitializeComponent();
if (LicenseManager.UsageMode != LicenseUsageMode.Designtime)
{
this.Width = double.NaN; ;
this.Height = double.NaN; ;
}
}
This checks to see if the control is running in Design-mode. If not (i.e. runtime) it will set the Width and Height to NaN (Not a number) which is the value you set it to if you remove the Width and Height attributes in XAML.
So at design-time you will have the preset width and height (including if you put the user control in a form) and at runtime it will dock depending on its parent container.
Hope that helps.

Here are a list of Design-Time Attributes in the Silverlight Designer. They are the same for the WPF designer.
It lists all of the d: values available in the Designer such as d:DesignHeight, d:DesignWidth, d:IsDesignTimeCreatable, d:CreateList and several others.

I do this all the time. Simply set the width and height values to "auto" where you instantiate your control, and this will override the design-time values for that UserControl.
ie: <loc:UserControl1 Width="auto" Height="auto" />
Another option is to set a combination of MinWidth and MinHeight to a size that allows design-time work, while Width and Height remain "auto". Obviously, this only works if you don't need the UserControl to size smaller than the min values at runtime.

I was looking for similar solution like the one used in Blend and with your mentions I created simple behavior class with two attached properties Width & Height that are applied only in DesinTime
public static class DesignBehavior
{
private static readonly Type OwnerType = typeof (DesignBehavior);
#region Width
public static readonly DependencyProperty WidthProperty =
DependencyProperty.RegisterAttached(
"Width",
typeof (double),
OwnerType,
new FrameworkPropertyMetadata(double.NaN, new PropertyChangedCallback(WidthChangedCallback)));
public static double GetWidth(DependencyObject depObj)
{
return (double)depObj.GetValue(WidthProperty);
}
public static void SetWidth(DependencyObject depObj, double value)
{
depObj.SetValue(WidthProperty, value);
}
private static void WidthChangedCallback(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
if (DesignerProperties.GetIsInDesignMode(depObj)) {
depObj.SetValue(FrameworkElement.WidthProperty, e.NewValue);
}
}
#endregion
#region Height
public static readonly DependencyProperty HeightProperty =
DependencyProperty.RegisterAttached(
"Height",
typeof (double),
OwnerType,
new FrameworkPropertyMetadata(double.NaN, new PropertyChangedCallback(HeightChangedCallback)));
public static double GetHeight(DependencyObject depObj)
{
return (double)depObj.GetValue(HeightProperty);
}
public static void SetHeight(DependencyObject depObj, double value)
{
depObj.SetValue(HeightProperty, value);
}
private static void HeightChangedCallback(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
if (DesignerProperties.GetIsInDesignMode(depObj)) {
depObj.SetValue(FrameworkElement.HeightProperty, e.NewValue);
}
}
#endregion
}
Then in your UserControl you just set these properties in Xaml
<UserControl x:Class="ExtendedDataGrid.Views.PersonOverviewView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:tool="http://schemas.microsoft.com/wpf/2008/toolkit"
xmlns:b="clr-namespace:ExtendedDataGrid.Behaviors"
b:DesignBehavior.Width="600" b:DesignBehavior.Height="200">
<Grid>
...
</Grid>
</UserControl>

Use MinWidth and MinHeight on the control. That way, you'll see it in the designer, and at runtime it will size the way you want.

I do it similar, but my solution assures that if you add your control to an container in design mode, it will appear reasonably.
protected override void OnVisualParentChanged(DependencyObject oldParent)
{
if (this.Parent != null)
{
this.Width = double.NaN;
this.Height = double.NaN;
}
}
what do you think?

Thanks to the original answerer for this solution! For those that are interested, here it is in VB:
If LicenseManager.UsageMode <> LicenseUsageMode.Designtime Then
Me.Width = Double.NaN
Me.Height = Double.NaN
End If

Some have suggested using the LicenseManager.UsageMode property which I've never seen before but I have used the following code.
if(!DesignerProperties.GetIsInDesignMode(this))
{
this.Width = double.NaN;
this.Height = double.NaN;
}
esskar,
I just want to add that you should generally always call the method of the base when overriding an "On" method.
protected override void OnVisualParentChanged(DependencyObject oldParent)
{
base.OnVisualParentChanged(oldParent);
...
}
Great workaround by the way, I'm using it myself now too.

Related

Microsoft WPF Designer Value For Property Not Updating When Changed By Another Property

Similar questions have been asked for WinForms projects, but not for WPF projects.
This VERY simple sample below shows the issue. The real user control I have is much more complicated but this sample shows the basic issue I am having.
The UserControl1 xaml (nothing there as it doesn't need anything to show the issue)
<UserControl x:Class="TestProperty.UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
</Grid>
</UserControl>
The Window xaml (just contains a UserControl1):
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:TestProperty="clr-namespace:TestProperty;assembly=TestProperty" x:Class="TestApp.MainWindow"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<TestProperty:UserControl1 HorizontalAlignment="Left" Height="100" Margin="10,10,0,0" VerticalAlignment="Top" Width="100"/>
</Grid>
</Window>
The actual Window code-behind is really nothing
using System.Windows;
namespace TestApp
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}
Here is code-behind for UserControl1. Has two properties where the first property simply modifies the second to be the same.
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
namespace TestProperty
{
/// <summary>
/// Interaction logic for UserControl1.xaml
/// </summary>
public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();
}
[Description("Sets a test property value that sets another property.")]
[Category("UserControl1")]
[RefreshProperties(RefreshProperties.All)]
public bool TestValue1
{
get => (bool) GetValue(TestValue1Property);
set => SetValue(TestValue1Property, value);
}
public static readonly DependencyProperty TestValue1Property =
DependencyProperty.Register(
"TestValue1", typeof(bool), typeof(UserControl1),
new FrameworkPropertyMetadata(false,
new PropertyChangedCallback(TestValue1Changed)));
private static void TestValue1Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) =>
((UserControl1) d)?.TestValue1Changed(e);
private void TestValue1Changed(DependencyPropertyChangedEventArgs e)
{
TestValue2 = (bool) e.NewValue;
InvalidateProperty(TestValue2Property);
}
[Description("Value should change automatically when TestValue1 changes.")]
[Category("UserControl1")]
[RefreshProperties(RefreshProperties.Repaint)]
public bool TestValue2
{
get => (bool) GetValue(TestValue2Property);
set => SetValue(TestValue2Property, value);
}
public static readonly DependencyProperty TestValue2Property =
DependencyProperty.Register(
"TestValue2", typeof(bool), typeof(UserControl1),
new FrameworkPropertyMetadata(false,
new PropertyChangedCallback(TestValue2Changed)));
private static void TestValue2Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) =>
((UserControl1) d)?.TestValue2Changed(e);
private void TestValue2Changed(DependencyPropertyChangedEventArgs e)
{
InvalidateProperty(TestValue2Property);
}
}
}
The attribute RefreshProperties and the call to InvalidateProperty appear to do nothing.
When editing the MainWindow.xaml and selecting UserControl1, the UserControl1 section of the Properties window shows TestValue1 and TestValue2. If TestValue1 is changed, TestValue2 does NOT change. However, if you change to another Visual Studio (or Blend) editor window and then back to the MainWindow.xaml, then TestValue2 will be the same as TestValue1.
I cannot seem to find any method to automatically have TestValue2 update when TestValue1 forces the update. The real case is obviously more complex with quite a few controls and when adding our UserControl, we want the designer to be able to specify several properties that can result in forcing (or coercing) other properties. However, the designer needs to be able to see the property value changes without having to switch to another file and back again to redraw the designer's properties window.
Is this something that simply can't be done - or is there something I'm missing. Note that this is a UserControl and is not designed to operate itself MVVM, but allow another window to be designed using the control with (or without) MVVM design.
Also, in a 'kind of' related issue. We have one property that needs to have an entry in the properties the same as Width and Height with the auto button. When set to Auto display "Auto (nnn)" as the automatically set value changes nnn changes. When not set, the value is simply nnn and is defined during the design of the window using the UserControl. Is this possible?
Thanks in advance to anyone who can shed light on this.

Window sizing constraints by content

I want the window to respect MinWidth/MinHeight and MaxWidth/MaxHeight specifications of the content control inside.
Some suggested using SizeToContent, but this only helps to set the initial window size, not the constraints.
Others suggested overriding MeasureOverride and setting window's Min/Max height and width there, but this seems to be somewhat unclean, considering that such a trivial problem should surely have a purely declarative solution.
Just to mention another solution which seems reasonable but does not work (and had been previously mentioned in an answer which got deleted): binding MinWidth of the window to MinWidth of the control does not take into account window decorations.
If the initial window size is set so that actual content size is not coerced by the content's MinWidth/MinHeight and MaxWidth/MaxHeight in the initial layout pass (for example, by using Window.SizeToContent="WidthAndHeight"), then following equations are true:
Window.ActualSize - Content.ActualSize =
Window.MinSize - Content.MinSize = Window.MaxSize - Content.MaxSize.
Based on these equations you can derive the following code:
public MainWindow()
{
InitializeComponent();
this.SizeChanged += OnWindowSizeChanged;
}
private static void OnWindowSizeChanged(object sender, SizeChangedEventArgs e)
{
var window = (Window)sender;
var content = (FrameworkElement)window.Content;
window.MinWidth = window.ActualWidth - content.ActualWidth + content.MinWidth;
window.MaxWidth = window.ActualWidth - content.ActualWidth + content.MaxWidth;
window.MinHeight = window.ActualHeight - content.ActualHeight + content.MinHeight;
window.MaxHeight = window.ActualHeight - content.ActualHeight + content.MaxHeight;
window.SizeChanged -= OnWindowSizeChanged;
}
I do not know how to achieve this efficiently using the pure declarative approach since the code should be ran just once after the initial layout pass.
Some suggested using SizeToContent, but this only helps to set the initial window size, not the constraints.
I worked around this by setting the MinWidth and MinHeight properties right after the windows was initialized:
MainWindow.xaml
<Window ... SizeToContent="WidthAndHeight">
...
</Window>
MainWindow.xaml.cs
public MainWindow()
{
InitializeComponent();
SourceInitialized += (s, e) =>
{
MinWidth = ActualWidth;
MinHeight = ActualHeight;
};
}
Use Binding markup extension. A binding is wpf's way of saying when this property (source) changes update some other property (target). In this case the Grid's MinWidth property is the Source and your window's MinWidth property is the target.
<Window x:Class="MinMaxValuesOnWindows.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="600" Width="800"
MinWidth="{Binding ElementName=gridy, Path=MinWidth}"
MinHeight="{Binding ElementName=gridy, Path=MinHeight}"
MaxWidth="{Binding ElementName=gridy, Path=MaxWidth}"
MaxHeight="{Binding ElementName=gridy, Path=MaxHeight}">
<Grid Name="gridy" MinHeight="80" MinWidth="80" MaxHeight="300" MaxWidth="300"/>
</Window>
As you mentioned in the topic this does not completely work, but you can use a converter on the binding to add on the window frame's height and width before updating the binding target (might require a PInvoke). Since I doubt the window frame thickness is dynamically changing in your application this can probably just be constant value (not necessarily true if user changes themes).

Set the focus on a textbox in xaml wpf

Despite some posts on this forum and others i cannot find something that tells me how to set the focus on a TextBox.
I have a userControl with many labels and textBoxes. When the form is loaded I want the a particular textBox to have the focus.
I have set the tabIndex but that didn't seem to work.
Any suggestions?
You can use the FocusManager.FocusedElement attached property for this purpose. Here's a piece of code that set the focus to TxtB by default.
<StackPanel Orientation="Vertical" FocusManager.FocusedElement="{Binding ElementName=TxtB}">
<TextBox x:Name="TxtA" Text="A" />
<TextBox x:Name="TxtB" Text="B" />
</StackPanel>
You can also use TxtB.Focus() in your code-behind if you don't want to do this in XAML.
You can apply this property directly on the TextBox :
<TextBox Text="{Binding MyText}" FocusManager.FocusedElement="{Binding RelativeSource={RelativeSource Self}}"/>
I am new to using WPF and reading through the above examples I had a similar experience trying set the focus to a textbox using the xaml code examples given, i.e. all the examples above didn't work.
What I found was I had to place the FocusManager.FocusElement in the page element. I assume this would probably work as well if you used a Window as the parent element. Anyway, here is the code that worked for me.
<Page x:Class="NameOfYourClass"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
Title="Title"
Height="720"
Width="915"
Background="white"
Loaded="pgLoaded"
FocusManager.FocusedElement="{Binding ElementName=NameOfYourTextBox}">
<!-- Create child elements here. -->
</Page>
I have a TextBox inside a Grid inside a DataTemplate which I want to have keyboard focus when it becomes visible. I also found that
<DataTemplate x:Key="DistanceView" DataType="{x:Type vm:ROI}">
<Grid FocusManager.FocusedElement="{Binding ElementName=tbDistance}">
<TextBox x:Name="tbDistance" Grid.Column="1" Grid.Row="1" VerticalAlignment="Bottom"/>
</Grid>
</DataTemplate>
did not work for me.
However when I call Focus() in the parent ContentControl
private void ContentControl_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if ((sender as ContentControl).IsVisible)
{
(sender as ContentControl).Focus();
}
}
it starts to work and the caret is visible in the TextBox. I think the FocusScope has to be given focus for the FocusManager.FocusedElement property to have any effect.
Jerry
From experimenting around, the xaml solution
FocusManager.FocusedElement="{Binding ElementName=yourElement}"
seems to work best when you place it in the highest element in the window hierarchy (usually Window, or the Grid you place everything else in)
Usage:
local:FocusManager.FocusOnLoad="True"
public class FocusManager
{
public static readonly DependencyProperty FocusOnLoad = DependencyProperty.RegisterAttached(
"FocusOnLoad",
typeof(bool),
typeof(FocusManager),
new UIPropertyMetadata(false, new PropertyChangedCallback(OnValueChanged))
);
private static void OnValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (!(sender is Control control))
return;
if ((bool) e.NewValue == false)
return;
control.Loaded += (s, e) => control.Focus();
}
public static bool GetFocusOnLoad(DependencyObject d) => (bool) d.GetValue(FocusOnLoad);
public static void SetFocusOnLoad(DependencyObject d, bool value) => d.SetValue(FocusOnLoad, value);
}
FocusManager was not in intellisense and this confused me a bit. I just typed the entire attribute and it worked.
FocusManager.FocusedElement="{Binding ElementName=MyTextBox}"
Microsoft Visual Studio Enterprise 2015 version 14.0.23107.0/C#/WPF
For completeness, there is also a way to handle this from code behind (e.g. in the case of controls that, for whatever reason, are created dynamically and don't exist in XAML). Attach a handler to the window's Loaded event and then use the ".Focus()" method of the control you want. Bare-bones example below.
public class MyWindow
{
private VisualCollection controls;
private TextBox textBox;
// constructor
public MyWindow()
{
controls = new VisualCollection(this);
textBox = new TextBox();
controls.Add(textBox);
Loaded += window_Loaded;
}
private void window_Loaded(object sender, RoutedEventArgs e)
{
textBox.Focus();
}
}
bind the element you want to point the focus in as
FocusManager.FocusedElement= "{Binding ElementName= Comobox1}"
in grid or groupbox etc
Further to my comment on Feb 04 '22, I solved it this way:
In the UserControl definitionin the XAML add a Loaded event handler. (pressing tab after Loaded= will automatically add an event handler to the code behind)
Then edit the event handler in the code behind:
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
expressionTextBox.Focus();
}
I'm hoping that WPF is clever enough to handle th unhooking of the evnt at some point, allowing the class to be garbage collected and not give rise to memory leaks, but I don't know. I'd be interested in any comments on that.

Silverlight - User Control Binding

I am learning Silverlight. In the process, I'm trying to build a custom user control. My ultimate goal is to be able to write the following statement in XAML:
<my:CustomControl>
<my:CustomControl.MainControl>
<Canvas><TextBlock Text="Hello!" /></Canvas>
</my:CustomControl.MainContent>
</my:CustomControl>
The content of the control will be wrapped in a custom border. Once again, this is just a learning exercise. To append my border, I have create the following UserControl:
<UserControl x:Class="CustomControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
<Grid x:Name="LayoutRoot">
<Border>
<!-- CustomControl Content -->
</Border>
</Grid>
</UserControl>
The code-behind for this file looks like the following:
public partial class CustomControl : UserControl
{
public UIElement MainContent
{
get { return (UIElement)GetValue(MainContentProperty); }
set { SetValue(MainContentProperty, value); }
}
public static readonly DependencyProperty MainContentProperty =
DependencyProperty.Register("MainContent", typeof(UIElement), typeof(CustomControl),
new PropertyMetadata(null));
public CustomControl()
{
InitializeComponent();
}
}
The thing I am having a problem with is getting the MainContent to appear in my CustomControl. I am confident that I am setting it properly, but I'm not sure how to display it. I really want it to be a DependencyProperty as well so I can take advantage of data binding and animations.
How do I get the MainContent to appear in the CustomControl? Thank you
First you need to wait until the rest of the control has been parsed so you need to hook the loaded event:-
public CustomControl()
{
InitializeComponent();
Loaded += new RoutedEventHandler(CustomControl_Loaded);
}
Then in the loaded event assign your MainControl property to the Child property of the border. To do that its best if you give your Border an x:Name which for now I'll simple call "border".
void CustomControl_Loaded(object sender, RoutedEventArgs e)
{
border.Child = MainControl;
}
That'll get you going. Of course you may need to deal with the MainControl property being changed dynamically so you need add a bool isLoaded field in your control and set that in the loaded event. When true your MainControl setter should assign the incoming value to the border.Child.
Things can start to get more complicated and in fact I don't recommend this approach to creating a custom control. For a better approach see Creating a New Control by Creating a ControlTemplate

Wpf ScrollViewer Scroll Amount

Is it possible to change the amount that the WPF ScrollViewer scrolls? I am simply wondering if it's possible to change the scrollviewer so that when using the mouse wheel or the scrollviewer arrows, the amount of incremental scrolling can be changed.
The short answer is: there is no way to do this without writing some custom scrolling code, but don't let that scare you it's not all that hard.
The ScrollViewer either works by scrolling using physical units (i.e. pixels) or by engaging with an IScrollInfo implementation to use logical units. This is controlled by the setting the CanContentScroll property where a value of false means "scroll the content using physical units" and a value of true means "scroll the content logically".
So how does the ScrollViewer scroll the content logically? By communicating with an IScrollInfo implementation. So that's how you could take over exactly how much the content of your panel scrolls when someone performs a logical action. Take a look at the documentation for IScrollInfo to get a listing of all the logical units of measurment that can be requested to scroll, but since you mentioned the mouse wheel you'll be mostly interested in the MouseWheelUp/Down/Left/Right methods.
Here's a simple, complete and working WPF ScrollViewer class that has a data-bindable SpeedFactor property for adjusting the mouse wheel sensitivity. Setting SpeedFactor to 1.0 means identical behavior to the WPF ScrollViewer. The default value for the dependency property is 2.5, which allows for very speedy wheel scrolling.
Of course, you can also create additional useful features by binding to the SpeedFactor property itself, i.e., to easily allow the user to control the multiplier.
public class WheelSpeedScrollViewer : ScrollViewer
{
public static readonly DependencyProperty SpeedFactorProperty =
DependencyProperty.Register(nameof(SpeedFactor),
typeof(Double),
typeof(WheelSpeedScrollViewer),
new PropertyMetadata(2.5));
public Double SpeedFactor
{
get { return (Double)GetValue(SpeedFactorProperty); }
set { SetValue(SpeedFactorProperty, value); }
}
protected override void OnPreviewMouseWheel(MouseWheelEventArgs e)
{
if (ScrollInfo is ScrollContentPresenter scp &&
ComputedVerticalScrollBarVisibility == Visibility.Visible)
{
scp.SetVerticalOffset(VerticalOffset - e.Delta * SpeedFactor);
e.Handled = true;
}
}
};
Complete XAML demo of 'fast mouse wheel scrolling' of around 3200 data items:
note: 'mscorlib' reference is only for accessing the demonstration data.
<UserControl x:Class="RemoveDuplicateTextLines.FastScrollDemo"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyApp"
xmlns:sys="clr-namespace:System;assembly=mscorlib">
<local:WheelSpeedScrollViewer VerticalScrollBarVisibility="Auto">
<ListBox ItemsSource="{Binding Source={x:Type sys:Object},Path=Assembly.DefinedTypes}" />
</local:WheelSpeedScrollViewer>
</UserControl>
Fast mouse wheel:
You could implement a behavior on the scrollviewer. In my case CanContentScroll did not work. The solution below works for scrolling with the mouse wheel as well as draging the scrollbar.
public class StepSizeBehavior : Behavior<ScrollViewer>
{
public int StepSize { get; set; }
#region Attach & Detach
protected override void OnAttached()
{
CheckHeightModulesStepSize();
AssociatedObject.ScrollChanged += AssociatedObject_ScrollChanged;
base.OnAttached();
}
protected override void OnDetaching()
{
AssociatedObject.ScrollChanged -= AssociatedObject_ScrollChanged;
base.OnDetaching();
}
#endregion
[Conditional("DEBUG")]
private void CheckHeightModulesStepSize()
{
var height = AssociatedObject.Height;
var remainder = height%StepSize;
if (remainder > 0)
{
throw new ArgumentException($"{nameof(StepSize)} should be set to a value by which the height van be divised without a remainder.");
}
}
private void AssociatedObject_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
const double stepSize = 62;
var scrollViewer = (ScrollViewer)sender;
var steps = Math.Round(scrollViewer.VerticalOffset / stepSize, 0);
var scrollPosition = steps * stepSize;
if (scrollPosition >= scrollViewer.ScrollableHeight)
{
scrollViewer.ScrollToBottom();
return;
}
scrollViewer.ScrollToVerticalOffset(scrollPosition);
}
}
You would use it like this:
<ScrollViewer MaxHeight="248"
VerticalScrollBarVisibility="Auto">
<i:Interaction.Behaviors>
<behaviors:StepSizeBehavior StepSize="62" />
</i:Interaction.Behaviors>
I wanted to add to Drew Marsh accepted answer - while the other suggested answers solve it, in some cases you cannot override the PreviewMouseWheel event and handle it without causing other side effects. Namely if you have child controls that should receive priority to be scrolled before the parent ScrollViewer - like nested ListBox or ComboBox popups.
In my scenario, my parent control was a ItemsControl with its ItemsPanel being a VirtualizingStackPanel. I wanted its logical scrolling to be 1 unit per item instead of the default 3. Instead of fiddling with attached behaviors and intercepting/handling the mouse wheel events, I simply implemented a custom VirtualizingStackPanel to do this.
public class VirtualizingScrollSingleItemAtATimeStackPanel : VirtualizingStackPanel
{
public override void MouseWheelDown()
{
PageDown();
}
public override void MouseWheelUp()
{
PageUp();
}
public override void PageDown()
{
LineDown();
}
public override void PageUp()
{
LineUp();
}
}
then we use that panel like we normally would in our xaml markup:
<ItemsControl>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:VirtualizingScrollSingleItemAtATimeStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Obviously my scenario is contrived and the solution very simple, however this might provide a path for others to have better control over the scrolling behavior without the side effects I encountered.
I did this to ensure whole numbers on scrollbar1.ValueChanged:
scrollbar1.Value = Math.Round(scrollbar1.Value, 0, MidpointRounding.AwayFromZero)

Resources