Canvas ActualWidth and ActualHeight passed into ViewModel in MVVM way - c#

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}">

Related

WPF Dependency property not being set at initialization

I'm trying to set width of UserControl at initialization time in ViewModel, but it receives only zero. When I resize the window, it receives correct width.
MainWindow.xaml extract:
<local:MapControl x:Name="MapControl"
DataContext="{Binding MapViewModel}"
ActualControlWidth="{Binding ActualControlWidth, Mode=OneWayToSource}" />
MapControl.xaml.cs:
public partial class MapControl : UserControl
{
public MapControl()
{
InitializeComponent();
SizeChanged += OnControlSizeChanged;
}
public static readonly DependencyProperty ActualControlWidthProperty = DependencyProperty.Register(
"ActualControlWidth",
typeof(double),
typeof(MapControl),
new FrameworkPropertyMetadata(PropertyChangedCallback)); //here it does not have any influence if i put default(double) or not
private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var newVar = e.NewValue;
}
public double ActualControlWidth
{
get { return (double)GetValue(ActualControlWidthProperty); }
set { SetValue(ActualControlWidthProperty, value); } //here comes the value as 785
}
private void OnControlSizeChanged(object sender, SizeChangedEventArgs e)
{
ActualControlWidth = ActualWidth;
}
}
MapViewModel.cs:
public class MapViewModel : ViewModelBase
{
private double _actualControlWidth;
private Map _map;
public MapViewModel()
{
...
}
public Map Map
{
get => _map;
set => Set(() => Map, ref _map, value);
}
public double ActualControlWidth
{
get => _actualControlWidth;
set => _actualControlWidth = value; //this is where the value comes as 0
}
}
Thanks for your help!
The full reproduction example is at my Github https://github.com/czechdude/dependencypropertyissue
You need to update this line:
new FrameworkPropertyMetadata(PropertyChangedCallback)); //here it does not have any influence if i put default(double) or not
To
new FrameworkPropertyMetadata(defaultValue: -1.0D,
new PropertyChangedCallback(MethodToCall));
...
private static void MethodToCall(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// call second method to make instance of MapControl and call a custom method that sets your ActualControlWidth
}

Zooming a plot via the MVVM pattern?

I have a complex Plot RenderingControl that I have placed into a View. What would be the ideal way to handle zooming with respect to the MVVM pattern? I want the user to be able to zoom by clicking and dragging on the plot.
One approach I see would be to take the MouseMove, MouseUp, MouseDown events of the Plot control and wire them up to commands in the PlotViewModel. Now in response to the commands the ViewModel could update it's ZoomLevel property, which could be bound to the view, and cause the View to zoom in. While the user is clicking and dragging I would also like to display a rectangle indicating the region that will be zoomed. Would it make sense to keep an AnnotationViewModel in PlotViewModel for the zoom preview?
Another approach would be to handle it all in the View and not involve the ViewModel at all.
The main difference I see is that capturing the behavior in the ViewModel will make that behavior much more re-useable than in the View. Though I have a feeling that the underlying Plot control and the resulting View are complex enough that there isn't going to be much of chance for re-use anyway. What do you think?
I think there are several ways to solve your problem. HighCore right, when he says that Zoom applies to View, so it is advisable to leave it on the side View. But there are alternatives, we consider them below. Unfortunately, I did not deal with Plot RenderingControl, so I will describe a solution based on an abstract, independent of the control.
AttachedBehavior
In this case, I would have tried to identify possible all the work with the control via an attached behavior, it is ideally suited for the MVVM pattern, and it can be used in the Blend.
Example of work
In your View, control is defined and an attached behavior, like so:
<RenderingControl Name="MyPlotControl"
AttachedBehaviors:ZoomBehavior.IsStart="True" ... />
And in code-behind:
public static class ZoomBehavior
{
public static readonly DependencyProperty IsStartProperty;
public static void SetIsStart(DependencyObject DepObject, bool value)
{
DepObject.SetValue(IsStartProperty, value);
}
public static bool GetIsStart(DependencyObject DepObject)
{
return (bool)DepObject.GetValue(IsStartProperty);
}
static ZoomBehavior()
{
IsStartMoveProperty = DependencyProperty.RegisterAttached("IsStart",
typeof(bool),
typeof(ZoomBehavior),
new UIPropertyMetadata(false, IsStart));
}
private static void IsStart(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
UIElement uiElement = sender as UIElement;
if (uiElement != null)
{
if (e.NewValue is bool && ((bool)e.NewValue) == true)
{
uiElement.MouseDown += new MouseButtonEventHandler(ObjectMouseDown);
uiElement.MouseMove += new MouseEventHandler(ObjectMouseMove);
uiElement.MouseUp += new MouseButtonEventHandler(ObjectMouseUp);
}
}
}
// Below is event handlers
}
Once you're set to true for property IsStart, PropertyChanged handler is triggered and it set the handlers for events that contain the basic logic.
For the transmission of additional data in you behavior register additional dependency properties, for example:
<RenderingControl Name="MyPlotControl"
AttachedBehaviors:ZoomBehavior.IsStart="True"
AttachedBehaviors:ZoomBehavior.ZoomValue="50" />
In code-behind:
// ... Here registered property
public static void SetZoomValue(DependencyObject DepObject, int value)
{
DepObject.SetValue(ZoomValueProperty, value);
}
public static int GetZoomValue(DependencyObject DepObject)
{
return (int)DepObject.GetValue(ZoomValueProperty);
}
// ... Somewhere in handler
int value = GetZoomValue(plotControl);
To retrieve data on the behavior, I use a singleton pattern. This pattern represents global static access point to the object and must guarantee the existence of a single instance of the class.
Example of using this pattern (taken from the behavior, who worked with the time display in the View):
public class TimeBehavior : INotifyPropertyChanged
{
// Global instance
private static TimeBehavior _instance = new TimeBehavior();
public static TimeBehavior Instance
{
get
{
return _instance;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private string _currentTime = DateTime.Now.ToString("HH:mm");
public string CurrentTime
{
get
{
return _currentTime;
}
set
{
if (_currentTime != value)
{
_currentTime = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("CurrentTime"));
}
}
}
}
private string _currentDayString = ReturnDayString();
public string CurrentDayString
{
get
{
return _currentDayString;
}
set
{
if (_currentDayString != value)
{
_currentDayString = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("CurrentDayString"));
}
}
}
}
private string _currentMonthAndDayNumber = ReturnMonthAndDayNumber();
public string CurrentMonthAndDayNumber
{
get
{
return _currentMonthAndDayNumber;
}
set
{
if (_currentMonthAndDayNumber != value)
{
_currentMonthAndDayNumber = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("CurrentMonthAndDayNumber"));
}
}
}
}
public static readonly DependencyProperty IsTimerStartProperty;
public static void SetIsTimerStart(DependencyObject DepObject, bool value)
{
DepObject.SetValue(IsTimerStartProperty, value);
}
public static bool GetIsTimerStart(DependencyObject DepObject)
{
return (bool)DepObject.GetValue(IsTimerStartProperty);
}
static void OnIsTimerStartPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue is bool && ((bool)e.NewValue) == true)
{
DispatcherTimer timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromMilliseconds(1000);
timer.Tick += new EventHandler(timer_Tick);
timer.Start();
}
}
static TimeBehavior()
{
IsTimerStartProperty = DependencyProperty.RegisterAttached("IsTimerStart",
typeof(bool),
typeof(TimeBehavior),
new PropertyMetadata(new PropertyChangedCallback(OnIsTimerStartPropertyChanged)));
}
private static void timer_Tick(object sender, EventArgs e)
{
_instance.CurrentTime = DateTime.Now.ToString("HH:mm");
_instance.CurrentDayString = ReturnDayString();
_instance.CurrentMonthAndDayNumber = ReturnMonthAndDayNumber();
}
}
Access to data in the View:
<TextBlock Name="WidgetTimeTextBlock"
Text="{Binding Path=CurrentTime,
Source={x:Static Member=AttachedBehaviors:TimeBehavior.Instance}}" />
Alternatives
Work in View via Interface
The point of this way is that we call a method in View via ViewModel, which does all the work, and he does not know about the View. This is accomplished by the operation of the interface and the well described here:
Talk to View
Using ServiceLocator
ServiceLocator allows you to work in the ViewModel, without violating the principles of MVVM. You have a RegisterService method where you register the instance of the service you want to provide and a GetService method which you would use to get the service you want.
More information can be found here:
Service Locator in MVVM

How to scroll to the bottom of a ScrollViewer automatically with Xaml and binding?

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.

Binding to a ScrollViewer's ViewportWidth and ViewportHeight

I am using the Model-View-ViewModel architecture in a WPF application I am building, and I would like a specific ViewModel to actually be reactive to the size of the view (not a normal use-case of the MVVM approach, I know).
Essentially, I have a ScrollViewer object and I want the viewmodel to observe the width and height of the scrollviewer and then be able to do things accordingly depending on what that width and height are.
I'd like to do something like this:
<ScrollViewer ViewportWidth="{Binding Path=MyViewportWidth, Mode=OneWayToSource}" ViewportHeight="{Binding Path=MyViewportHeight, Mode=OneWayToSource}" />
But of course this is impossible to do because "ViewportWidth" and "ViewportHeight" cannot be "bound to" (a.k.a. act as binding targets) because they are read-only dependency properties (even though I am not writing to them at all in this binding since it is OneWayToSource).
Anyone know of a good method to be able to do something like this?
You could try running something OnLoaded or OnResizeChanged that updates the viewmodel
private void ScrollViewer_Loaded(object sender, RoutedEventArgs e)
{
ScrollViewer sv = sender as ScrollViewer;
ViewModel vm = sv.DataContext as ViewModel;
vm.ScrollViewerHeight = sv.ViewportHeight;
vm.ScrollViewerWidth = sv.ViewportWidth;
}
Ok, this is a really old question, but I thought I'd share for posterity, since I've solved this one myself. The best solution I've found is to create a user control that derives from the ScrollView class and implements the properties you want - which are of course linked to the non-bindable properties of the base class.
You can use the OnPropertyChanged function to monitor those properties and keep the values in sync.
Here's the full code-behind of my custom usercontrol called DynamicScrollViewer. Notice that I have four bindable dependency properties called DynamicHorizontalOffset, DynamicVerticalOffset, DynamicViewportWidth, and DynamicViewportHeight.
The two offset properties allow both read and write control of the offset, while the viewport properties are essentially read-only.
I had to use this class when creating a complex animation editor control in which various components (labels at the left, nodes in the middle, timeline at top) needed to scroll synchronously, but only in limited aspects, and were all bound to common external scrollbars. Think of locking a section of rows in spreadsheet, and you get the idea.
using System.Windows;
using System.Windows.Controls;
namespace CustomControls
{
public partial class DynamicScrollViewer : ScrollViewer
{
public DynamicScrollViewer()
{
InitializeComponent();
}
public double DynamicHorizontalOffset
{
get { return (double)GetValue(DynamicHorizontalOffsetProperty); }
set { SetValue(DynamicHorizontalOffsetProperty, value); }
}
public static readonly DependencyProperty DynamicHorizontalOffsetProperty =
DependencyProperty.Register("DynamicHorizontalOffset", typeof(double), typeof(DynamicScrollViewer));
public double DynamicVerticalOffset
{
get { return (double)GetValue(DynamicVerticalOffsetProperty); }
set { SetValue(DynamicVerticalOffsetProperty, value); }
}
public static readonly DependencyProperty DynamicVerticalOffsetProperty =
DependencyProperty.Register("DynamicVerticalOffset", typeof(double), typeof(DynamicScrollViewer));
public double DynamicViewportWidth
{
get { return (double)GetValue(DynamicViewportWidthProperty); }
set { SetValue(DynamicViewportWidthProperty, value); }
}
public static readonly DependencyProperty DynamicViewportWidthProperty =
DependencyProperty.Register("DynamicViewportWidth", typeof(double), typeof(DynamicScrollViewer));
public double DynamicViewportHeight
{
get { return (double)GetValue(DynamicViewportHeightProperty); }
set { SetValue(DynamicViewportHeightProperty, value); }
}
public static readonly DependencyProperty DynamicViewportHeightProperty =
DependencyProperty.Register("DynamicViewportHeight", typeof(double), typeof(DynamicScrollViewer));
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.Property == DynamicVerticalOffsetProperty)
{
if (ScrollInfo != null)
ScrollInfo.SetVerticalOffset(DynamicVerticalOffset);
}
else if (e.Property == DynamicHorizontalOffsetProperty)
{
if (ScrollInfo != null)
ScrollInfo.SetHorizontalOffset(DynamicHorizontalOffset);
}
else if (e.Property == HorizontalOffsetProperty)
{
DynamicHorizontalOffset = (double)e.NewValue;
}
else if (e.Property == VerticalOffsetProperty)
{
DynamicVerticalOffset = (double)e.NewValue;
}
else if (e.Property == ViewportWidthProperty)
{
DynamicViewportWidth = (double)e.NewValue;
}
else if (e.Property == ViewportHeightProperty)
{
DynamicViewportHeight = (double)e.NewValue;
}
}
}
}

Two-Way Binding Of VerticalOffset Property on ScrollViewer?

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;
}
}
}

Categories

Resources