I have a View and a ViewModel in Silverlight 3.0.
The view contains a standard ScrollViewer, which contains dynamic content.
Depending on the content within the ScrollViewer, the user could have scrolled half way down the content, and then performed an action that causes the ScrollViewer to load new content, but the ScrollViewer does not automatically scroll to the top.
I want to be able to bind to the VerticalOffset property, but it is read-only. Any ideas on attachable behavior?
Any ideas?
Thanks.
The following blog post provides an attached behaviour that exposes the vertical / horizontal offsets of a scrollviewer so that you can bind to them, or set them in code:
http://blog.scottlogic.com/2010/07/21/exposing-and-binding-to-a-silverlight-scrollviewers-scrollbars.html
This allows the following markup:
<ScrollViewer
local:ScrollViewerBinding.VerticalOffset="{Binding YPosition, Mode=TwoWay}"
local:ScrollViewerBinding.HorizontalOffset="{Binding XPosition, Mode=TwoWay}">
<!-- Big content goes here! -->
</ScrollViewer>
Since you are using a ViewModel I take it the "action that causes ScrollViewer to load new content" is a result of changes made inside or to the ViewModel. That being the case I would add an event to the ViewModel that gets fired every time such a change occurs.
Your View can the add a handler on this event and call ScrollToVerticalPosition on the ScrollViewer when its fired.
I've simplified the #ColinE's solution. Instead of hooking to the ScrollBar.ValueChanged event, I hook to the ScrollViewer.ScrollChanged event. So, 1. it is not necessary to find the ScrollBar in the visual tree and 2. ScrollBar.ValueChanged is called in some transition states when the content of the ScrollViewer changes and I do not want to catch these states.
I post my code for the VerticalOffset, the HorizontalOffset is similar:
/// <summary>
/// VerticalOffset attached property
/// </summary>
public static readonly DependencyProperty VerticalOffsetProperty =
DependencyProperty.RegisterAttached("VerticalOffset", typeof(double),
typeof(ScrollViewerBinding), new FrameworkPropertyMetadata(double.NaN,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnVerticalOffsetPropertyChanged));
/// <summary>
/// Just a flag that the binding has been applied.
/// </summary>
private static readonly DependencyProperty VerticalScrollBindingProperty =
DependencyProperty.RegisterAttached("VerticalScrollBinding", typeof(bool?), typeof(ScrollViewerBinding));
public static double GetVerticalOffset(DependencyObject depObj)
{
return (double)depObj.GetValue(VerticalOffsetProperty);
}
public static void SetVerticalOffset(DependencyObject depObj, double value)
{
depObj.SetValue(VerticalOffsetProperty, value);
}
private static void OnVerticalOffsetPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
ScrollViewer scrollViewer = d as ScrollViewer;
if (scrollViewer == null)
return;
BindVerticalOffset(scrollViewer);
scrollViewer.ScrollToVerticalOffset((double)e.NewValue);
}
public static void BindVerticalOffset(ScrollViewer scrollViewer)
{
if (scrollViewer.GetValue(VerticalScrollBindingProperty) != null)
return;
scrollViewer.SetValue(VerticalScrollBindingProperty, true);
scrollViewer.ScrollChanged += (s, se) =>
{
if (se.VerticalChange == 0)
return;
SetVerticalOffset(scrollViewer, se.VerticalOffset);
};
}
And use it in the XAML:
<ScrollViewer local:ScrollViewerBinding.VerticalOffset="{Binding ScrollVertical}">
<!-- content ... -->
</ScrollViewer>
I started with this but noticed there isn't a cleanup phase so here goes the complete implementation with both horizontal and vertical offsets bindable:
using System.Windows;
using System.Windows.Controls;
namespace Test
{
public static class ScrollPositionBehavior
{
public static readonly DependencyProperty HorizontalOffsetProperty =
DependencyProperty.RegisterAttached(
"HorizontalOffset",
typeof(double),
typeof(ScrollPositionBehavior),
new FrameworkPropertyMetadata(
double.NaN,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnHorizontalOffsetPropertyChanged));
public static readonly DependencyProperty VerticalOffsetProperty =
DependencyProperty.RegisterAttached(
"VerticalOffset",
typeof(double),
typeof(ScrollPositionBehavior),
new FrameworkPropertyMetadata(
double.NaN,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnVerticalOffsetPropertyChanged));
private static readonly DependencyProperty IsScrollPositionBoundProperty =
DependencyProperty.RegisterAttached("IsScrollPositionBound", typeof(bool?), typeof(ScrollPositionBehavior));
public static void BindOffset(ScrollViewer scrollViewer)
{
if (scrollViewer.GetValue(IsScrollPositionBoundProperty) is true)
return;
scrollViewer.SetValue(IsScrollPositionBoundProperty, true);
scrollViewer.Loaded += ScrollViewer_Loaded;
scrollViewer.Unloaded += ScrollViewer_Unloaded;
}
public static double GetHorizontalOffset(DependencyObject depObj)
{
return (double)depObj.GetValue(HorizontalOffsetProperty);
}
public static double GetVerticalOffset(DependencyObject depObj)
{
return (double)depObj.GetValue(VerticalOffsetProperty);
}
public static void SetHorizontalOffset(DependencyObject depObj, double value)
{
depObj.SetValue(HorizontalOffsetProperty, value);
}
public static void SetVerticalOffset(DependencyObject depObj, double value)
{
depObj.SetValue(VerticalOffsetProperty, value);
}
private static void OnHorizontalOffsetPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ScrollViewer scrollViewer = d as ScrollViewer;
if (scrollViewer == null || double.IsNaN((double)e.NewValue))
return;
BindOffset(scrollViewer);
scrollViewer.ScrollToHorizontalOffset((double)e.NewValue);
}
private static void OnVerticalOffsetPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ScrollViewer scrollViewer = d as ScrollViewer;
if (scrollViewer == null || double.IsNaN((double)e.NewValue))
return;
BindOffset(scrollViewer);
scrollViewer.ScrollToVerticalOffset((double)e.NewValue);
}
private static void ScrollChanged(object s, ScrollChangedEventArgs se)
{
if (se.VerticalChange != 0)
SetVerticalOffset(s as ScrollViewer, se.VerticalOffset);
if (se.HorizontalChange != 0)
SetHorizontalOffset(s as ScrollViewer, se.HorizontalOffset);
}
private static void ScrollViewer_Loaded(object sender, RoutedEventArgs e)
{
var scrollViewer = sender as ScrollViewer;
scrollViewer.ScrollChanged += ScrollChanged;
}
private static void ScrollViewer_Unloaded(object sender, RoutedEventArgs e)
{
var scrollViewer = sender as ScrollViewer;
scrollViewer.SetValue(IsScrollPositionBoundProperty, false);
scrollViewer.ScrollChanged -= ScrollChanged;
scrollViewer.Loaded -= ScrollViewer_Loaded;
scrollViewer.Unloaded -= ScrollViewer_Unloaded;
}
}
}
Related
Recently I've encountered a problem with a seemingly easy taks: I wanted to use Mode=OneWayToSource to push Width and Height parameters of a canvas into a ViewModel in one application that I work on. Turns out Mode=OneWayToSource does not work for that and according to Microsoft it is a feature of WPF that was intended. Ok, sure.
Anyway, I've been scratching my head about this for some time thinking about how to do this the best ( that is the shortest and the least messy) way while adhering to the MVVM principle. I've figured the following:
/// <summary>
/// Sends graph dimensions to <see cref="GraphViewModel"/>
/// </summary>
public class SendGraphDimensionsToViewModelProperty : BaseAttachedProperty<SendGraphDimensionsToViewModelProperty, bool>
{
public override void OnValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (!(sender is Canvas canvas))
return;
canvas.SizeChanged += (sender, e) =>
{
if (canvas.DataContext is GraphViewModel graph)
{
graph.Width = canvas.ActualWidth;
graph.Height = canvas.ActualHeight;
}
};
}
}
Then i just attach the property to a canvas with DataContext of GraphViewModel instance:
local:SendGraphDimensionsToViewModelProperty.Value="True"
This works as intended. Basically the attached property's value changes from null to true on load which triggers the SizeChanged event that monitors the ActualHeight and ActualWidth of the Canvas.
My question is: Would you suggest any improvements to my code (related to possible memory leaks for instance and such)? I would just like to "MinMax" this while learning something new. I am a junior dev and I cannot really consult this with someone IRL, as nobody I know does MVVM. I am trying to write everything in a modular fashion, meaning I want to write it once and then just subsequently copy it when I need it again to speed up the rate at which I can develop apps.
Looking forward to your suggestions.
My solution for this issue was a group of attached properties, which can be applied to any FrameworkElement instance.
public static class perSizeBindingHelper
{
public static readonly DependencyProperty ActiveProperty = DependencyProperty.RegisterAttached(
"Active",
typeof(bool),
typeof(perSizeBindingHelper),
new FrameworkPropertyMetadata(OnActiveChanged));
public static bool GetActive(FrameworkElement frameworkElement)
{
return (bool) frameworkElement.GetValue(ActiveProperty);
}
public static void SetActive(FrameworkElement frameworkElement, bool active)
{
frameworkElement.SetValue(ActiveProperty, active);
}
public static readonly DependencyProperty BoundActualWidthProperty = DependencyProperty.RegisterAttached(
"BoundActualWidth",
typeof(double),
typeof(perSizeBindingHelper));
public static double GetBoundActualWidth(FrameworkElement frameworkElement)
{
return (double) frameworkElement.GetValue(BoundActualWidthProperty);
}
public static void SetBoundActualWidth(FrameworkElement frameworkElement, double width)
{
frameworkElement.SetValue(BoundActualWidthProperty, width);
}
public static readonly DependencyProperty BoundActualHeightProperty = DependencyProperty.RegisterAttached(
"BoundActualHeight",
typeof(double),
typeof(perSizeBindingHelper));
public static double GetBoundActualHeight(FrameworkElement frameworkElement)
{
return (double) frameworkElement.GetValue(BoundActualHeightProperty);
}
public static void SetBoundActualHeight(FrameworkElement frameworkElement, double height)
{
frameworkElement.SetValue(BoundActualHeightProperty, height);
}
private static void OnActiveChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
if (!(dependencyObject is FrameworkElement frameworkElement))
{
return;
}
if ((bool) e.NewValue)
{
frameworkElement.SizeChanged += OnFrameworkElementSizeChanged;
UpdateObservedSizesForFrameworkElement(frameworkElement);
}
else
{
frameworkElement.SizeChanged -= OnFrameworkElementSizeChanged;
}
}
private static void OnFrameworkElementSizeChanged(object sender, SizeChangedEventArgs e)
{
if (sender is FrameworkElement frameworkElement)
{
UpdateObservedSizesForFrameworkElement(frameworkElement);
}
}
private static void UpdateObservedSizesForFrameworkElement(FrameworkElement frameworkElement)
{
frameworkElement.SetCurrentValue(BoundActualWidthProperty, frameworkElement.ActualWidth);
frameworkElement.SetCurrentValue(BoundActualHeightProperty, frameworkElement.ActualHeight);
}
}
Usage is ...
<Grid ...
vhelp:perSizeBindingHelper.Active="True"
vhelp:perSizeBindingHelper.BoundActualHeight="{Binding GridHeight, Mode=OneWayToSource}"
vhelp:perSizeBindingHelper.BoundActualWidth="{Binding GridWidth, Mode=OneWayToSource}">
We have a WPF application which has a query count result displayed on the screen. We initially defined the result as a button so that when it was clicked, the application would display a detailed list of the query results. However, for reasons unrelated to this question, we now need this to be a border (basically, just the template for the original button). So far, I have set up my attached property:
public static class AttachedCommandBehavior
{
#region Command
public static DependencyProperty PreviewMouseLeftButtonUpCommandProperty = DependencyProperty.RegisterAttached(
"PreviewMouseLeftButtonUpCommand",
typeof(ICommand),
typeof(AttachedCommandBehavior),
new FrameworkPropertyMetadata(PreviewPreviewMouseLeftButtonUpChanged));
public static void SetPreviewMouseLeftButtonUpChanged(DependencyObject target, ICommand value)
{
target.SetValue(PreviewMouseLeftButtonUpCommandProperty, value);
}
public static ICommand GetPreviewMouseLeftButtonUpChanged(DependencyObject target)
{
return (ICommand)target.GetValue(PreviewMouseLeftButtonUpCommandProperty);
}
private static void PreviewPreviewMouseLeftButtonUpChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is UIElement element)
{
if (e.NewValue != null && e.OldValue == null)
{
element.PreviewMouseLeftButtonUp += element_PreviewMouseLeftButtonUp;
}
else if (e.NewValue == null && e.OldValue != null)
{
element.PreviewMouseLeftButtonUp -= element_PreviewMouseLeftButtonUp;
}
}
}
private static void element_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (sender is UIElement element)
{
if (element.GetValue(PreviewMouseLeftButtonUpCommandProperty) is ICommand command)
command.Execute(CommandParameterProperty);
}
}
#endregion
#region CommandParameter
public static DependencyProperty CommandParameterProperty = DependencyProperty.RegisterAttached(
"CommandParameter",
typeof(object),
typeof(AttachedCommandBehavior),
new FrameworkPropertyMetadata(CommandParameterChanged));
public static void SetCommandParameter(DependencyObject target, object value)
{
target.SetValue(CommandParameterProperty, value);
}
public static object GetCommandParameter(DependencyObject target)
{
return target.GetValue(CommandParameterProperty);
}
private static void CommandParameterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is UIElement element)
{
element.SetValue(CommandParameterProperty, e.NewValue);
}
}
#endregion
}
And then in my XAML, I am trying to bind my command to the attached DependencyProperty:
<Border Background="{Binding BackgroundColor, Converter={StaticResource ColorNameToBrushConverter}}"
Cursor="{x:Static Cursors.Hand}"
local:AttachedCommandBehavior.PreviewMouseLeftButtonUpChanged="{Binding QueryClickedCommand}">
<Grid>...</Grid>
</Border>
However, my little blue squiggly line is telling me "A 'Binding' cannot be used within a 'Border' collection. A 'Binding' can only be set on a DependencyProperty of a DependencyObject." Being the daring programmer that I am, I boldly ignore the little blue squiggly and try to run anyway. At which point I get an exception:
System.Windows.Markup.XamlParseException: 'A 'Binding' cannot be set on the 'SetPreviewMouseLeftButtonUpChanged' property of type 'Viewbox'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject.'
It turns out this was a naming convention problem. In copying/pasting/renaming/general indecision, I messed up the names of my getter and setter for the command property. Once I changed them all to match the correct pattern, my code runs.
#region Command
public static DependencyProperty PreviewMouseLeftButtonUpCommandProperty = DependencyProperty.RegisterAttached(
"PreviewMouseLeftButtonUpCommand",
typeof(ICommand),
typeof(AttachedCommandBehavior),
new FrameworkPropertyMetadata(PreviewMouseLeftButtonUpChanged));
public static void SetPreviewMouseLeftButtonUpCommand(DependencyObject target, ICommand value)
{
target.SetValue(PreviewMouseLeftButtonUpCommandProperty, value);
}
public static ICommand GetPreviewMouseLeftButtonUpCommand(DependencyObject target)
{
return (ICommand)target.GetValue(PreviewMouseLeftButtonUpCommandProperty);
}
private static void PreviewMouseLeftButtonUpChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is UIElement element)
{
if (e.NewValue != null && e.OldValue == null)
{
element.PreviewMouseLeftButtonUp += element_PreviewMouseLeftButtonUp;
}
else if (e.NewValue == null && e.OldValue != null)
{
element.PreviewMouseLeftButtonUp -= element_PreviewMouseLeftButtonUp;
}
}
}
private static void element_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (sender is UIElement element)
{
if (element.GetValue(PreviewMouseLeftButtonUpCommandProperty) is ICommand command)
command.Execute(CommandParameterProperty);
}
}
#endregion
Hi I'm trying to bind to the TextBox.CaretIndex property which isn't a DependencyProperty, so I created a Behavior, but it doesn't work as expected.
Expectation (when focused)
default = 0
if I change the value in my view it should change the value in my viewmodel
if I change the value in my viewmodel it should change the value in my view
Current behavior
viewmodel value gets called ones when the window opens
Code-behind
public class TextBoxBehavior : DependencyObject
{
public static readonly DependencyProperty CursorPositionProperty =
DependencyProperty.Register(
"CursorPosition",
typeof(int),
typeof(TextBoxBehavior),
new FrameworkPropertyMetadata(
default(int),
new PropertyChangedCallback(CursorPositionChanged)));
public static void SetCursorPosition(DependencyObject dependencyObject, int i)
{
// breakpoint get never called
dependencyObject.SetValue(CursorPositionProperty, i);
}
public static int GetCursorPosition(DependencyObject dependencyObject)
{
// breakpoint get never called
return (int)dependencyObject.GetValue(CursorPositionProperty);
}
private static void CursorPositionChanged(
DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
// breakpoint get never called
//var textBox = dependencyObject as TextBox;
//if (textBox == null) return;
}
}
XAML
<TextBox Text="{Binding TextTemplate,UpdateSourceTrigger=PropertyChanged}"
local:TextBoxBehavior.CursorPosition="{Binding CursorPosition}"/>
Further Information
I think there is something really wrong here because I need to derive it from DependencyObject which was never needed before, because CursorPositionProperty is already a DependencyProperty, so this should be enough. I also think I need to use some events in my Behavior to set my CursorPositionProperty correctly, but I don't know which.
After fighting with my Behavior i can present you a 99% working solution
Behavior
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace WpfMVVMTextBoxCursorPosition
{
public class TextBoxCursorPositionBehavior : DependencyObject
{
public static void SetCursorPosition(DependencyObject dependencyObject, int i)
{
dependencyObject.SetValue(CursorPositionProperty, i);
}
public static int GetCursorPosition(DependencyObject dependencyObject)
{
return (int)dependencyObject.GetValue(CursorPositionProperty);
}
public static readonly DependencyProperty CursorPositionProperty =
DependencyProperty.Register("CursorPosition"
, typeof(int)
, typeof(TextBoxCursorPositionBehavior)
, new FrameworkPropertyMetadata(default(int))
{
BindsTwoWayByDefault = true
,DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
}
);
public static readonly DependencyProperty TrackCaretIndexProperty =
DependencyProperty.RegisterAttached(
"TrackCaretIndex",
typeof(bool),
typeof(TextBoxCursorPositionBehavior),
new UIPropertyMetadata(false
, OnTrackCaretIndex));
public static void SetTrackCaretIndex(DependencyObject dependencyObject, bool i)
{
dependencyObject.SetValue(TrackCaretIndexProperty, i);
}
public static bool GetTrackCaretIndex(DependencyObject dependencyObject)
{
return (bool)dependencyObject.GetValue(TrackCaretIndexProperty);
}
private static void OnTrackCaretIndex(DependencyObject dependency, DependencyPropertyChangedEventArgs e)
{
var textbox = dependency as TextBox;
if (textbox == null)
return;
bool oldValue = (bool)e.OldValue;
bool newValue = (bool)e.NewValue;
if (!oldValue && newValue) // If changed from false to true
{
textbox.SelectionChanged += OnSelectionChanged;
}
else if (oldValue && !newValue) // If changed from true to false
{
textbox.SelectionChanged -= OnSelectionChanged;
}
}
private static void OnSelectionChanged(object sender, RoutedEventArgs e)
{
var textbox = sender as TextBox;
if (textbox != null)
SetCursorPosition(textbox, textbox.CaretIndex); // dies line does nothing
}
}
}
XAML
<TextBox Height="50" VerticalAlignment="Top"
Name="TestTextBox"
Text="{Binding MyText}"
vm:TextBoxCursorPositionBehavior.TrackCaretIndex="True"
vm:TextBoxCursorPositionBehavior.CursorPosition="{Binding CursorPosition,Mode=TwoWay}"/>
<TextBlock Height="50" Text="{Binding CursorPosition}"/>
there is just on thing i don't know why it doesn't work => BindsTwoWayByDefault = true. it has no effect on the binding as far as i can tell you because of this i need to set the binding mode explicit in XAML
I encountered a similar problem, and the easiest solution for me was to inherit from TextBox and add a DependencyProperty. So it looks like this:
namespace UI.Controls
{
public class MyTextBox : TextBox
{
public static readonly DependencyProperty CaretPositionProperty =
DependencyProperty.Register("CaretPosition", typeof(int), typeof(MyTextBox),
new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnCaretPositionChanged));
public int CaretPosition
{
get { return (int)GetValue(CaretPositionProperty); }
set { SetValue(CaretPositionProperty, value); }
}
public MyTextBox()
{
SelectionChanged += (s, e) => CaretPosition = CaretIndex;
}
private static void OnCaretPositionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
(d as MyTextBox).CaretIndex = (int)e.NewValue;
}
}
}
... and in my XAML:
xmlns:controls="clr-namespace:IU.Controls"
...
<controls:MyTextBox CaretPosition="{Binding CaretPosition}"/>
... and CaretPosition property in the View Model of course. If you're not going to bind your View Model to other text-editing controls, this may be sufficient, if yes - you'll probably need another solution.
The solution from WiiMaxx has the following problems for me:
The caret index in the text box is not changed when the view model property is changed from the code.
This was also mentioned by Tejas Vaishnav in his comment to the solution.
BindsTwoWayByDefault = true does not work.
He stated that it is strange that he needs to inherit from DependencyObject.
The TrackCaretIndex property is only used for initialisation and it felt kind of unnecessary.
Here is my solution which solves those problems:
Behavior
public static class TextBoxAssist
{
// This strange default value is on purpose it makes the initialization problem very unlikely.
// If the default value matches the default value of the property in the ViewModel,
// the propertyChangedCallback of the FrameworkPropertyMetadata is initially not called
// and if the property in the ViewModel is not changed it will never be called.
private const int CaretIndexPropertyDefault = -485609317;
public static void SetCaretIndex(DependencyObject dependencyObject, int i)
{
dependencyObject.SetValue(CaretIndexProperty, i);
}
public static int GetCaretIndex(DependencyObject dependencyObject)
{
return (int)dependencyObject.GetValue(CaretIndexProperty);
}
public static readonly DependencyProperty CaretIndexProperty =
DependencyProperty.RegisterAttached(
"CaretIndex",
typeof(int),
typeof(TextBoxAssist),
new FrameworkPropertyMetadata(
CaretIndexPropertyDefault,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
CaretIndexChanged));
private static void CaretIndexChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs eventArgs)
{
if (dependencyObject is not TextBox textBox || eventArgs.OldValue is not int oldValue || eventArgs.NewValue is not int newValue)
{
return;
}
if (oldValue == CaretIndexPropertyDefault && newValue != CaretIndexPropertyDefault)
{
textBox.SelectionChanged += SelectionChangedForCaretIndex;
}
else if (oldValue != CaretIndexPropertyDefault && newValue == CaretIndexPropertyDefault)
{
textBox.SelectionChanged -= SelectionChangedForCaretIndex;
}
if (newValue != textBox.CaretIndex)
{
textBox.CaretIndex = newValue;
}
}
private static void SelectionChangedForCaretIndex(object sender, RoutedEventArgs eventArgs)
{
if (sender is TextBox textBox)
{
SetCaretIndex(textBox, textBox.CaretIndex);
}
}
}
XAML
<TextBox Height="50" VerticalAlignment="Top"
Name="TestTextBox"
Text="{Binding MyText}"
viewModels:TextBoxAssist.CaretIndex="{Binding CaretIndex}"/>
Some clarifications for the differences:
View model property changes work now because the caret index on the TextBox is set at the end of CaretIndexChanged.
The BindsTwoWayByDefault was fixed by using the according FrameworkPropertyMetadata constructor parameter.
Inheriting from DependencyObject was only necessary because DependencyProperty.Register was used instead of DependencyProperty.RegisterAttached.
Without the TrackCaretIndex property I had the problem that the propertyChangedCallback for the FrameworkPropertyMetadata was never called to properly initialize things. The problem occurs only when the default value for the FrameworkPropertyMetadata match the value of the view model property right from the start and the view model property is not changed. That's why I used this random default value.
As you said, the TextBox.CaretIndex Property is not a DependencyProperty, so you cannot data bind to it. Even with your own DependencyProperty, it won't work... how would you expect to be notified when TextBox.CaretIndex Property changes?
I'm trying to pass an object created in MainWindow to my UserControl that will read and modify it but it doesn't don't know why. Here is the code I'm using:
MainWindow class:
public partial class MainWindow : Window
{
public SupremeLibrary.Player player = new SupremeLibrary.Player();
public MainWindow()
{
InitializeComponent();
MusicSeekBar = new Components.SeekBar(player);
}
}
And SeekBar user control:
public partial class SeekBar : UserControl
{
DispatcherTimer Updater = new DispatcherTimer();
SupremeLibrary.Player player;
/// <summary>
/// Initialize new Seekbar
/// </summary>
public SeekBar()
{
InitializeComponent();
InitializeUpdater();
}
public SeekBar(SupremeLibrary.Player _player)
{
player = _player;
InitializeComponent();
InitializeUpdater();
}
private void InitializeUpdater()
{
Updater.Interval = TimeSpan.FromMilliseconds(100);
Updater.Tick += UpdateSeekBar;
Updater.Start();
}
private void UpdateSeekBar(object sender, EventArgs e)
{
if (player != null)
{
if (player.PlaybackState == SupremeLibrary.PlaybackStates.Playing)
{
if (player.Position.TotalMilliseconds != CustomProgressBar.Value) CustomProgressBar.Value = player.Position.TotalMilliseconds;
if (player.MaxPosition.TotalMilliseconds != CustomProgressBar.Maximum) CustomProgressBar.Maximum = player.MaxPosition.TotalMilliseconds;
}
}
}
private void PB_SeekBar_ChangeValue(object obj, MouseEventArgs e)
{
if (player != null)
{
if (player.PlaybackState == SupremeLibrary.PlaybackStates.Playing)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
player.Position = TimeSpan.FromMilliseconds(e.GetPosition(obj as ProgressBar).X / ((obj as ProgressBar).ActualWidth / 100) * ((obj as ProgressBar).Maximum / 100));
}
}
}
}
In add, it works if I use
public SupremeLibrary.Player player = new SupremeLibrary.Player();
as static and call it in UserControl as MainWindow.player but it's ugly and I don't want to use it.
I have tried to pass player from MainWindow as reference but it doesn't seem to work either.
Example using MediaElement
user control SeekBar
XAML
<UserControl x:Class="CSharpWPF.SeekBar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
DataContext="{Binding RelativeSource={RelativeSource Self}}" >
<Slider Maximum="{Binding TotalMilliseconds}"
Value="{Binding CurrentPosition}"/>
</UserControl>
i have defined a Slider in the control with binding to the maximum and value property to the TotalMilliseconds and CurrentPosition of the control, the properties will be bound to the control itself as I have set the DataContext of the control to self
.cs
public partial class SeekBar : UserControl
{
DispatcherTimer Updater = new DispatcherTimer();
/// <summary>
/// Initialize new Seekbar
/// </summary>
public SeekBar()
{
InitializeComponent();
InitializeUpdater();
}
private void InitializeUpdater()
{
Updater.Interval = TimeSpan.FromMilliseconds(100);
Updater.Tick += UpdateSeekBar;
}
public MediaElement Player
{
get { return (MediaElement)GetValue(PlayerProperty); }
set { SetValue(PlayerProperty, value); }
}
// Using a DependencyProperty as the backing store for Player. This enables animation, styling, binding, etc...
public static readonly DependencyProperty PlayerProperty =
DependencyProperty.Register("Player", typeof(MediaElement), typeof(SeekBar), new PropertyMetadata(null, OnPlayerChanged));
private static void OnPlayerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
SeekBar seekB = d as SeekBar;
if (e.OldValue != null)
{
SeekBar oldSeekB = (e.OldValue as SeekBar);
oldSeekB.Player.MediaOpened -= seekB.Player_MediaOpened;
oldSeekB.Player.MediaEnded -= seekB.Player_MediaEnded;
}
if (seekB.Player != null)
{
seekB.Player.MediaOpened += seekB.Player_MediaOpened;
seekB.Player.MediaEnded += seekB.Player_MediaEnded;
}
}
void Player_MediaEnded(object sender, RoutedEventArgs e)
{
Updater.Stop();
}
private void Player_MediaOpened(object sender, RoutedEventArgs e)
{
if (Player.NaturalDuration.HasTimeSpan)
{
TotalMilliseconds = Player.NaturalDuration.TimeSpan.TotalMilliseconds;
Updater.Start();
}
else
{
CurrentPosition = 0.0;
TotalMilliseconds = 1.0;
}
}
public double CurrentPosition
{
get { return (double)GetValue(CurrentPositionProperty); }
set { SetValue(CurrentPositionProperty, value); }
}
// Using a DependencyProperty as the backing store for CurrentPosition. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CurrentPositionProperty =
DependencyProperty.Register("CurrentPosition", typeof(double), typeof(SeekBar), new PropertyMetadata(1.0, OnCurrentPositionChange));
private static void OnCurrentPositionChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
SeekBar seekB = d as SeekBar;
if (seekB.Player != null)
{
seekB.Player.Position = TimeSpan.FromMilliseconds(seekB.CurrentPosition);
}
}
public double TotalMilliseconds
{
get { return (double)GetValue(TotalMillisecondsProperty); }
set { SetValue(TotalMillisecondsProperty, value); }
}
// Using a DependencyProperty as the backing store for TotalMilliseconds. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TotalMillisecondsProperty =
DependencyProperty.Register("TotalMilliseconds", typeof(double), typeof(SeekBar), new PropertyMetadata(0.0));
private void UpdateSeekBar(object sender, EventArgs e)
{
if (Player != null && TotalMilliseconds > 1)
{
CurrentPosition = Player.Position.TotalMilliseconds;
}
}
}
what I have done
defined property Player of Media Element to be bound at UI
attached MediaOpened and MediaEnded for the purpose of starting and stopping the timer adn updating the duration.
defined properties for current position and total duration for my slider control in the control UI
and on change of CurrentPosition, I'll update back the player's position.
usage in main window
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<MediaElement x:Name="media"
Source="Wildlife.wmv" />
<l:SeekBar Grid.Row="1"
Player="{Binding ElementName=media}" />
</Grid>
I'll just bind the media element to the Player property of my SeekBar control
by doing in this way I've not done any hard coding in code behind, also by means of interface you can achieve a greater decoupling between your seekbar and the player
this is just a simple example for your case, you may use your custom player and progress control in the above example to achieve your results.
I've got a TextBlock whose content is data bound to a string property of the ViewModel. This TextBlock has a ScrollViewer wrapped around it.
What I want to do is every time the logs change, the ScrollViewer will scroll to the bottom. Ideally I want something like this:
<ScrollViewer ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollPosition="{Binding Path=ScrollPosition}">
<TextBlock Text="{Binding Path=Logs}"/>
</ScrollViewer>
I don't want to use Code Behind! The solution I'm looking for should be using only binding and/or Xaml.
You can either create an attached property or a behavior to achieve what you want without using code behind. Either way you will still need to write some code.
Here is an example of using attached property.
Attached Property
public static class Helper
{
public static bool GetAutoScroll(DependencyObject obj)
{
return (bool)obj.GetValue(AutoScrollProperty);
}
public static void SetAutoScroll(DependencyObject obj, bool value)
{
obj.SetValue(AutoScrollProperty, value);
}
public static readonly DependencyProperty AutoScrollProperty =
DependencyProperty.RegisterAttached("AutoScroll", typeof(bool), typeof(Helper), new PropertyMetadata(false, AutoScrollPropertyChanged));
private static void AutoScrollPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var scrollViewer = d as ScrollViewer;
if (scrollViewer != null && (bool)e.NewValue)
{
scrollViewer.ScrollToBottom();
}
}
}
Xaml Binding
<ScrollViewer local:Helper.AutoScroll="{Binding IsLogsChangedPropertyInViewModel}" .../>
You will need to create a boolean property IsLogsChangedPropertyInViewModel and set it to true when the string property is changed.
Hope this helps! :)
Answer updated 2017-12-13, now uses the ScrollChanged event and checks if the size of extent changes. More reliable and doesn't interfere with manual scrolling
I know this question is old, but I've got an improved implementation:
No external dependencies
You only need to set the property once
The code is heavily influenced by Both Justin XL's and Contango's solutions
public static class AutoScrollBehavior
{
public static readonly DependencyProperty AutoScrollProperty =
DependencyProperty.RegisterAttached("AutoScroll", typeof(bool), typeof(AutoScrollBehavior), new PropertyMetadata(false, AutoScrollPropertyChanged));
public static void AutoScrollPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var scrollViewer = obj as ScrollViewer;
if(scrollViewer != null && (bool)args.NewValue)
{
scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged;
scrollViewer.ScrollToEnd();
}
else
{
scrollViewer.ScrollChanged-= ScrollViewer_ScrollChanged;
}
}
private static void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
// Only scroll to bottom when the extent changed. Otherwise you can't scroll up
if (e.ExtentHeightChange != 0)
{
var scrollViewer = sender as ScrollViewer;
scrollViewer?.ScrollToBottom();
}
}
public static bool GetAutoScroll(DependencyObject obj)
{
return (bool)obj.GetValue(AutoScrollProperty);
}
public static void SetAutoScroll(DependencyObject obj, bool value)
{
obj.SetValue(AutoScrollProperty, value);
}
}
Usage:
<ScrollViewer n:AutoScrollBehavior.AutoScroll="True" > // Where n is the XML namespace
From Geoff's Blog on ScrollViewer AutoScroll Behavior.
Add this class:
namespace MyAttachedBehaviors
{
/// <summary>
/// Intent: Behavior which means a scrollviewer will always scroll down to the bottom.
/// </summary>
public class AutoScrollBehavior : Behavior<ScrollViewer>
{
private double _height = 0.0d;
private ScrollViewer _scrollViewer = null;
protected override void OnAttached()
{
base.OnAttached();
this._scrollViewer = base.AssociatedObject;
this._scrollViewer.LayoutUpdated += new EventHandler(_scrollViewer_LayoutUpdated);
}
private void _scrollViewer_LayoutUpdated(object sender, EventArgs e)
{
if (Math.Abs(this._scrollViewer.ExtentHeight - _height) > 1)
{
this._scrollViewer.ScrollToVerticalOffset(this._scrollViewer.ExtentHeight);
this._height = this._scrollViewer.ExtentHeight;
}
}
protected override void OnDetaching()
{
base.OnDetaching();
if (this._scrollViewer != null)
{
this._scrollViewer.LayoutUpdated -= new EventHandler(_scrollViewer_LayoutUpdated);
}
}
}
}
This code depends Blend Behaviors, which require a reference to System.Windows.Interactivity. See help on adding System.Windows.Interactivity.
If you install the MVVM Light NuGet package, you can add a reference here:
packages\MvvmLightLibs.4.2.30.0\lib\net45\System.Windows.Interactivity.dll
Ensure that you have this property in your header, which points to System.Windows.Interactivity.dll:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
Add a Blend Behavior into the ScrollViewer:
<i:Interaction.Behaviors>
<implementation:AutoScrollBehavior />
</i:Interaction.Behaviors>
Example:
<GroupBox Grid.Row="2" Header ="Log">
<ScrollViewer>
<i:Interaction.Behaviors>
<implementation:AutoScrollBehavior />
</i:Interaction.Behaviors>
<TextBlock Margin="10" Text="{Binding Path=LogText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextWrapping="Wrap"/>
</ScrollViewer>
</GroupBox>
We have to add a definition for the namespace, or else it won't know where to find the C# class we have just added. Add this property into the <Window> tag. If you are using ReSharper, it will automatically suggest this for you.
xmlns:implementation="clr-namespace:MyAttachedBehaviors"
Now, if all goes well, the text in the box will always scroll down to the bottom.
The example XAML given will print the contents of the bound property LogText to the screen, which is perfect for logging.
It is easy, examples:
yourContronInside.ScrollOwner.ScrollToEnd ();
yourContronInside.ScrollOwner.ScrollToBottom ();
Here is a slight variation.
This will scroll to the bottom both when the scroll viewer height (viewport) and the height of it's scroll presenter's content (extent) change.
It's based on Roy T's answer but I wasn't able to comment so I have posted as an answer.
public static class AutoScrollHelper
{
public static readonly DependencyProperty AutoScrollProperty =
DependencyProperty.RegisterAttached("AutoScroll", typeof(bool), typeof(AutoScrollHelper), new PropertyMetadata(false, AutoScrollPropertyChanged));
public static void AutoScrollPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var scrollViewer = obj as ScrollViewer;
if (scrollViewer == null) return;
if ((bool) args.NewValue)
{
scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged;
scrollViewer.ScrollToEnd();
}
else
{
scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged;
}
}
static void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
// Remove "|| e.ViewportHeightChange < 0 || e.ExtentHeightChange < 0" if you want it to only scroll to the bottom when it increases in size
if (e.ViewportHeightChange > 0 || e.ExtentHeightChange > 0 || e.ViewportHeightChange < 0 || e.ExtentHeightChange < 0)
{
var scrollViewer = sender as ScrollViewer;
scrollViewer?.ScrollToEnd();
}
}
public static bool GetAutoScroll(DependencyObject obj)
{
return (bool) obj.GetValue(AutoScrollProperty);
}
public static void SetAutoScroll(DependencyObject obj, bool value)
{
obj.SetValue(AutoScrollProperty, value);
}
}
I was using #Roy T. 's answer, however I wanted the added stipulation that if you scrolled back in time, but then added text, the scroll view should auto scroll to bottom.
I used this:
private static void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
var scrollViewer = sender as ScrollViewer;
if (e.ExtentHeightChange > 0)
{
scrollViewer.ScrollToEnd();
}
}
in place of the SizeChanged event.