WPF DependencyProperty Firing only once - wpf

I am having issues with one of my dependency properties not firing on change. Set up is as follows. I have a class that acts as a mediator for a ScrollViewer, in there I have a dependency property for a ScrollViewer type, and 2 doubles, VerticalOffsetNew, and VerticalScrollOffset. So I attach a previewmousewheel event to grab events coming from the mouse wheel.
In this event i grab the scrollviewers current offset and set VerticalScrollOffset to that value. Then depending on the delta I set the VerticalOffsetNew to be offset+delta.
Now the issue I am seeing is the following. the VerticalScrollOffset does not seem to get set after the first time. I have a onpropertychange event for that and the breakpoint is only hit the first time. The other property that is being set in the preview event (verticaloffsetnew) gets set everytime no problem.
any ideas?
public double VerticalScrollOffset
{
get { return (double)GetValue(VerticalScrollOffsetProperty); }
set { SetValue(VerticalScrollOffsetProperty, value); }
}
public static readonly DependencyProperty VerticalScrollOffsetProperty =
DependencyProperty.Register("VerticalScrollOffset", typeof(double),
typeof(ScrollViewerOffsetMediator), new PropertyMetadata(OnVerticalScrollOffsetChanged));
public static void OnVerticalScrollOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ScrollViewerOffsetMediator mediator = (ScrollViewerOffsetMediator)d;
}
Above is my DP that is only set once in the preview code, below is the preview code.
private void CustomPreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
double mouseWheelChange = (double)e.Delta;
ScrollViewer thisScroller = (ScrollViewer)sender;
// not sure why the delta is so large here
//double newVOffset = this.VerticalOffset - (mouseWheelChange / 40);
// forcing scroll wheel to iterate by 8
mouseWheelChange /= Math.Abs(mouseWheelChange);
double newVOffset = thisScroller.VerticalOffset - (mouseWheelChange * 8);
// this does not trigger a change after the first time for some
// some strange reason !!!!!!!
this.VerticalScrollOffset = thisScroller.VerticalOffset;
if (newVOffset < 0)
{
this.VerticalOffset = 0;
}
else if (newVOffset > thisScroller.ScrollableHeight)
{
this.VerticalOffset = thisScroller.ScrollableHeight;
}
else
{
this.VerticalOffset = newVOffset;
}
e.Handled = true;
}
Thanks for any help.
Update
So there is something else I have noticed, the preview mouse call updates both the VerticalOffset and the VerticalScrollOffset. Both of which are DP's, now the thing that is interesting is that in the change event of the VerticalOffset there is a call to an animate method that will animate the VerticalScrollOffset DP. It seems that when I remove that call both DP's change correctly but as soon as I have that it fails. Is there some restriction or conflicts that may be arising, that I dont see or WPF silently tries to resolve?
Thanks again.

Have you tried implementing INotifyPropertyChanged?

Related

Get the scroll position of a WPF TextBox

I need to add some decoration to the contents of a WPF TextBox control. That works fine basically, I can get the position of specified character indices and layout my other elements accordingly. But it all breaks when the TextBox is scrolled. My layout positions don't match with the displayed text anymore because it has moved elsewhere.
Now I'm pretty surprised that the TextBox class doesn't provide any information about its scrolling state, nor any events when the scrolling has changed. What can I do now?
I used Snoop to find out whether there is some scrolling sub-element that I could ask, but the ScrollContentPresenter also doesn't have any scrolling information available. I'd really like to put my decoration elements right into the scrolled area so that the scrolling can affect them, too, but there can only be a single content control and that's one of the TextBox internals already.
I'm not sure how to capture an event when the textbox has been scrolled (probably use narohi's answer for that), but there is a simple way to see what the current scroll position is:
// Gets or sets the vertical scroll position.
textBox.VerticalOffset
(From http://msdn.microsoft.com/en-us/library/system.windows.controls.primitives.textboxbase.verticaloffset(v=vs.100).aspx)
I'm using it to see if the textbox is scrolled to the end, like this:
public static bool IsScrolledToEnd(this TextBox textBox)
{
return textBox.VerticalOffset + textBox.ViewportHeight == textBox.ExtentHeight;
}
You can get the ScrollViewer with this method by passing in your textbox as the argument and the type ScrollView. Then you may subscribe to the ScrollChanged event.
public static T FindDescendant<T>(DependencyObject obj) where T : DependencyObject
{
if (obj == null) return default(T);
int numberChildren = VisualTreeHelper.GetChildrenCount(obj);
if (numberChildren == 0) return default(T);
for (int i = 0; i < numberChildren; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
if (child is T)
{
return (T)(object)child;
}
}
for (int i = 0; i < numberChildren; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
var potentialMatch = FindDescendant<T>(child);
if (potentialMatch != default(T))
{
return potentialMatch;
}
}
return default(T);
}
Example:
public MainWindow()
{
InitializeComponent();
Loaded += new RoutedEventHandler(MainWindow_Loaded);
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
ScrollViewer s = FindDescendant<ScrollViewer>(txtYourTextBox);
s.ScrollChanged += new ScrollChangedEventHandler(s_ScrollChanged);
}
void s_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
// check event args for information needed
}

How do I throttle a slider's value change event?

I got a slider that on value change forces a fairly serious computation, so I want to throttle it to fire actual event after for example 50ms pass when user has finished sliding it.
While I learned some various stuff about Rx its unclear how should I approach this using MVVM pattern.
In my current MVVM approach I got slider value bound to my viewModel. I would prefer to add Rx throttle with minimal possible impact on existing code (as a beginning at least).
Ive seen some other threads about MVVM and Rx and I don't think they lead me to some exact direction with my problem. I see various possible approaches and would like not to invent a bycicle.
In this case, you should bind to the PropertyChanged event of your ViewModel, something like:
Observable.FromEvent<PropertyChangedEventArgs>(x => this.PropertyChanged +=x, x => this.PropertyChanged -= x)
.Where(x => x.PropertyName == "SliderName")
.Select(_ => this.SliderName)
.Throttle(TimeSpan.FromMilliseconds(50));
Or, if you were using ReactiveUI, it'd look like this:
this.WhenAnyValue(x => x.SliderName)
.Throttle(TimeSpan.FromMilliseconds(50), RxApp.DeferredScheduler);
Lets just outline the problem. You have a View Model which has some double typed Property. When a value is assigned to this property a fairly expensive calculation takes place. Wouldn't normally be a problem but when the UI binds the value of a Slider to this property the rapid changes generated does create a problem.
First decision to be made is between the view and view-model which is responsible for dealing with this problem. It could be argued both ways the View-Model has "chosen" to make a property assignment an expensice operatione on the other hand the View has "chosen" to assign the property using a Slider.
My choice would be on view side of things because thats a better place to implement this. However rather than fiddle with the View directly I would build a new Control to add the feature. Let's call it the DelaySlider. It will derive from Silder and have two additional dependency properties Delay and DelayedValue. The DelayedValue will match the existing value of Value property but only after Delay milliseconds have elapsed since the last Value changed.
Here is the full code for the control:-
public class DelaySlider : Slider
{
private DispatcherTimer myTimer;
private bool myChanging = false;
#region public double DelayedValue
public double DelayedValue
{
get { return (double)GetValue(DelayedValueProperty); }
set { SetValue(DelayedValueProperty, value); }
}
public static readonly DependencyProperty DelayedValueProperty =
DependencyProperty.Register(
"DelayedValue",
typeof(double),
typeof(DelaySlider),
new PropertyMetadata(0.0, OnDelayedValuePropertyChanged));
private static void OnDelayedValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DelaySlider source = d as DelaySlider;
if (source != null && !source.myChanging)
{
source.Value = (double)e.NewValue;
}
}
#endregion public double DelayedValue
#region public int Delay
public int Delay
{
get { return (int)GetValue(DelayProperty); }
set { SetValue(DelayProperty, value); }
}
public static readonly DependencyProperty DelayProperty =
DependencyProperty.Register(
"Delay",
typeof(int),
typeof(DelaySlider),
new PropertyMetadata(0, OnDelayPropertyChanged));
private static void OnDelayPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DelaySlider source = d as DelaySlider;
if (source != null)
{
source.OnDelayPropertyChanged((int)e.OldValue, (int)e.NewValue);
}
}
private void OnDelayPropertyChanged(int oldValue, int newValue)
{
if (myTimer != null)
{
myTimer.Stop();
myTimer = null;
}
if (newValue > 0)
{
myTimer = new DispatcherTimer();
myTimer.Tick += myTimer_Tick;
myTimer.Interval = TimeSpan.FromMilliseconds(newValue);
}
}
void myTimer_Tick(object sender, EventArgs e)
{
myTimer.Stop();
myChanging = true;
SetValue(DelayedValueProperty, Value);
myChanging = false;
}
#endregion public int Delay
protected override void OnValueChanged(double oldValue, double newValue)
{
base.OnValueChanged(oldValue, newValue);
if (myTimer != null)
{
myTimer.Start();
}
}
}
Now replace your Silder with DelaySlider and bind your View-Model property to the DelayedValue and specify your millisecond delay value in its Delay property.
You now have a useful re-usable control, you haven't messed about with nasty tricks in the View, you have no additional code in the code-behind of the view, the View-Model is unchanged and undisturbed and you haven't had to do include the Rx stuff at all.

How can I make WPF ScrollViewer middle-click-scroll?

Clicking the middle mouse button (aka: mouse wheel) and then moving the mouse down slightly lets users scroll in IE, and most Windows apps. This behavior appears to be missing in WPF controls by default? Is there a setting, a workaround, or something obvious that I'm missing?
I have found how to achieve this using 3 mouse events (MouseDown, MouseUp, MouseMove). Their handlers are attached to the ScrollViewer element in the xaml below:
<Grid>
<ScrollViewer MouseDown="ScrollViewer_MouseDown" MouseUp="ScrollViewer_MouseUp" MouseMove="ScrollViewer_MouseMove">
<StackPanel x:Name="dynamicLongStackPanel">
</StackPanel>
</ScrollViewer>
<Canvas x:Name="topLayer" IsHitTestVisible="False" />
</Grid>
It would be better to write a behaviour instead of events in code-behind, but not everyone has the necessary library, and also I don't know how to connect it with the Canvas.
The event handlers:
private bool isMoving = false; //False - ignore mouse movements and don't scroll
private bool isDeferredMovingStarted = false; //True - Mouse down -> Mouse up without moving -> Move; False - Mouse down -> Move
private Point? startPosition = null;
private double slowdown = 200; //The number 200 is found from experiments, it should be corrected
private void ScrollViewer_MouseDown(object sender, MouseButtonEventArgs e)
{
if (this.isMoving == true) //Moving with a released wheel and pressing a button
this.CancelScrolling();
else if (e.ChangedButton == MouseButton.Middle && e.ButtonState == MouseButtonState.Pressed)
{
if (this.isMoving == false) //Pressing a wheel the first time
{
this.isMoving = true;
this.startPosition = e.GetPosition(sender as IInputElement);
this.isDeferredMovingStarted = true; //the default value is true until the opposite value is set
this.AddScrollSign(e.GetPosition(this.topLayer).X, e.GetPosition(this.topLayer).Y);
}
}
}
private void ScrollViewer_MouseUp(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Middle && e.ButtonState == MouseButtonState.Released && this.isDeferredMovingStarted != true)
this.CancelScrolling();
}
private void CancelScrolling()
{
this.isMoving = false;
this.startPosition = null;
this.isDeferredMovingStarted = false;
this.RemoveScrollSign();
}
private void ScrollViewer_MouseMove(object sender, MouseEventArgs e)
{
var sv = sender as ScrollViewer;
if (this.isMoving && sv != null)
{
this.isDeferredMovingStarted = false; //standard scrolling (Mouse down -> Move)
var currentPosition = e.GetPosition(sv);
var offset = currentPosition - startPosition.Value;
offset.Y /= slowdown;
offset.X /= slowdown;
//if(Math.Abs(offset.Y) > 25.0/slowdown) //Some kind of a dead space, uncomment if it is neccessary
sv.ScrollToVerticalOffset(sv.VerticalOffset + offset.Y);
sv.ScrollToHorizontalOffset(sv.HorizontalOffset + offset.X);
}
}
If to remove the method calls AddScrollSign and RemoveScrollSign this example will work. But I have extended it with 2 methods which set scroll icon:
private void AddScrollSign(double x, double y)
{
int size = 50;
var img = new BitmapImage(new Uri(#"d:\middle_button_scroll.png"));
var adorner = new Image() { Source = img, Width = size, Height = size };
//var adorner = new Ellipse { Stroke = Brushes.Red, StrokeThickness = 2.0, Width = 20, Height = 20 };
this.topLayer.Children.Add(adorner);
Canvas.SetLeft(adorner, x - size / 2);
Canvas.SetTop(adorner, y - size / 2);
}
private void RemoveScrollSign()
{
this.topLayer.Children.Clear();
}
Example of icons:
And one last remark: there are some problems with the way Press -> Immediately Release -> Move. It is supposed to cancel scrolling if a user clicks the mouse left button, or any key of keyboard, or the application looses focus. There are many events and I don't have time to handle them all.
But standard way Press -> Move -> Release works without problems.
vorrtex posted a nice solution, please upvote him!
I do have some suggestions for his solution though, that are too lengthy to fit them all in comments, that's why I post a separate answer and direct it to him!
You mention problems with Press->Release->Move. You should use MouseCapturing to get the MouseEvents even when the Mouse is not over the ScrollViewer anymore. I have not tested it, but I guess your solution also fails in Press->Move->Move outside of ScrollViewer->Release, Mousecapturing will take care of that too.
Also you mention using a Behavior. I'd rather suggest an attached behavior that doesn't need extra dependencies.
You should definately not use an extra Canvas but do this in an Adorner.
The ScrollViewer itsself hosts a ScrollContentPresenter that defines an AdornerLayer. You should insert the Adorner there. This removes the need for any further dependency and also keeps the attached behavior as simple as IsMiddleScrollable="true".

Silvelright -set tabindex of UIElements in MVVM

I am trying to set the tab index of two UIElements within a user control. The user control contains a text box and button. I have focus currently being applied to the textbox via an attached property however I would like to have the ability to press the tab key and navigate from the textblock to the button or detect the key press (Enter key) and trigger the command on the button(I know separate question)
The main focus is accomplishing the tab index first.
Thanks for any pointers or suggestions.
UPDATE
I've since tried to employ an attached property to handle the tabbing order
public static DependencyProperty TabIndexProperty = DependencyProperty.RegisterAttached("TabIndex", typeof(int), typeof(AttachedProperties), null);
public static void SetTabIndex(UIElement element, int value)
{
Control c = element as Control;
if (c != null)
{
RoutedEventHandler loadedEventHandler = null;
loadedEventHandler = new RoutedEventHandler(delegate
{
HtmlPage.Plugin.Focus();
c.Loaded -= loadedEventHandler;
c.Focus();
});
c.Loaded += loadedEventHandler;
}
}
However when this I attempt to compile I receive errors that the TabIndex property does not exist for the button control. Any ideas why this is failing?
This is a view specific concern and, as such, even in MVVM should be handled at the ViewLevel. MVVM doesn't stipulate that you remove all code from code behind. It simply means you should have a view specific concern when you do put code there. This is one of those cases, imo.
It is late in the day... I resolved this using an attached property. in the above solution I had copied an earlier DP that I created and did not change the code before I tested.
Below is the working solution
I created a attached properties class and then added the following code:
#region Search Field Focus
public static DependencyProperty InitialFocusProperty = DependencyProperty.RegisterAttached("InitialFocus", typeof(bool), typeof(AttachedProperties), null);
public static void SetInitialFocus(UIElement element, bool value)
{
Control c = element as Control;
if (c != null && value)
{
RoutedEventHandler loadedEventHandler = null;
//set focus on control
loadedEventHandler = new RoutedEventHandler(delegate
{
HtmlPage.Plugin.Focus();
c.Loaded -= loadedEventHandler;
c.Focus();
});
c.Loaded += loadedEventHandler;
}
}
public static bool GetInitialFocus(UIElement element)
{
return false;
}
#endregion
#region Tabbing Order of Elements
public static DependencyProperty TabIndexProperty = DependencyProperty.RegisterAttached("TabIndex", typeof(int), typeof(AttachedProperties), null);
public static void SetTabIndex(UIElement element, int value)
{
element.SetValue(TabIndexProperty, value);
}
public static int GetTabIndex(UIElement element)
{
return (int)element.GetValue(TabIndexProperty);
}
#endregion
The first DP sets the focus of a textblock so that when the user control is loaded you see the cursor placed within the text field.
DP 2 sets the tabbing order. Since the focus is already applied to the current control tabbing falls into place normally. If you did not have focus on the control you would need to set this first.
then finally within the xaml declare your class in the xmlns and add away to the controls.

How can I show scrollbars on a System.Windows.Forms.TextBox only when the text doesn't fit?

For a System.Windows.Forms.TextBox with Multiline=True, I'd like to only show the scrollbars when the text doesn't fit.
This is a readonly textbox used only for display. It's a TextBox so that users can copy the text out. Is there anything built-in to support auto show of scrollbars? If not, should I be using a different control? Or do I need to hook TextChanged and manually check for overflow (if so, how to tell if the text fits?)
Not having any luck with various combinations of WordWrap and Scrollbars settings. I'd like to have no scrollbars initially and have each appear dynamically only if the text doesn't fit in the given direction.
#nobugz, thanks, that works when WordWrap is disabled. I'd prefer not to disable wordwrap, but it's the lesser of two evils.
#André Neves, good point, and I would go that way if it was user-editable. I agree that consistency is the cardinal rule for UI intuitiveness.
I came across this question when I wanted to solve the same problem.
The easiest way to do it is to change to System.Windows.Forms.RichTextBox. The ScrollBars property in this case can be left to the default value of RichTextBoxScrollBars.Both, which indicates "Display both a horizontal and a vertical scroll bar when needed." It would be nice if this functionality were provided on TextBox.
Add a new class to your project and paste the code shown below. Compile. Drop the new control from the top of the toolbox onto your form. It's not quite perfect but ought to work for you.
using System;
using System.Drawing;
using System.Windows.Forms;
public class MyTextBox : TextBox {
private bool mScrollbars;
public MyTextBox() {
this.Multiline = true;
this.ReadOnly = true;
}
private void checkForScrollbars() {
bool scroll = false;
int cnt = this.Lines.Length;
if (cnt > 1) {
int pos0 = this.GetPositionFromCharIndex(this.GetFirstCharIndexFromLine(0)).Y;
if (pos0 >= 32768) pos0 -= 65536;
int pos1 = this.GetPositionFromCharIndex(this.GetFirstCharIndexFromLine(1)).Y;
if (pos1 >= 32768) pos1 -= 65536;
int h = pos1 - pos0;
scroll = cnt * h > (this.ClientSize.Height - 6); // 6 = padding
}
if (scroll != mScrollbars) {
mScrollbars = scroll;
this.ScrollBars = scroll ? ScrollBars.Vertical : ScrollBars.None;
}
}
protected override void OnTextChanged(EventArgs e) {
checkForScrollbars();
base.OnTextChanged(e);
}
protected override void OnClientSizeChanged(EventArgs e) {
checkForScrollbars();
base.OnClientSizeChanged(e);
}
}
I also made some experiments, and found that the vertical bar will always show if you enable it, and the horizontal bar always shows as long as it's enabled and WordWrap == false.
I think you're not going to get exactly what you want here. However, I believe that users would like better Windows' default behavior than the one you're trying to force. If I were using your app, I probably would be bothered if my textbox real-estate suddenly shrinked just because it needs to accomodate an unexpected scrollbar because I gave it too much text!
Perhaps it would be a good idea just to let your application follow Windows' look and feel.
There's an extremely subtle bug in nobugz's solution that results in a heap corruption, but only if you're using AppendText() to update the TextBox.
Setting the ScrollBars property from OnTextChanged will cause the Win32 window (handle) to be destroyed and recreated. But OnTextChanged is called from the bowels of the Win32 edit control (EditML_InsertText), which immediately thereafter expects the internal state of that Win32 edit control to be unchanged. Unfortunately, since the window is recreated, that internal state has been freed by the OS, resulting in an access violation.
So the moral of the story is: don't use AppendText() if you're going to use nobugz's solution.
I had some success with the code below.
public partial class MyTextBox : TextBox
{
private bool mShowScrollBar = false;
public MyTextBox()
{
InitializeComponent();
checkForScrollbars();
}
private void checkForScrollbars()
{
bool showScrollBar = false;
int padding = (this.BorderStyle == BorderStyle.Fixed3D) ? 14 : 10;
using (Graphics g = this.CreateGraphics())
{
// Calcualte the size of the text area.
SizeF textArea = g.MeasureString(this.Text,
this.Font,
this.Bounds.Width - padding);
if (this.Text.EndsWith(Environment.NewLine))
{
// Include the height of a trailing new line in the height calculation
textArea.Height += g.MeasureString("A", this.Font).Height;
}
// Show the vertical ScrollBar if the text area
// is taller than the control.
showScrollBar = (Math.Ceiling(textArea.Height) >= (this.Bounds.Height - padding));
if (showScrollBar != mShowScrollBar)
{
mShowScrollBar = showScrollBar;
this.ScrollBars = showScrollBar ? ScrollBars.Vertical : ScrollBars.None;
}
}
}
protected override void OnTextChanged(EventArgs e)
{
checkForScrollbars();
base.OnTextChanged(e);
}
protected override void OnResize(EventArgs e)
{
checkForScrollbars();
base.OnResize(e);
}
}
What Aidan describes is almost exactly the UI scenario I am facing. As the text box is read only, I don't need it to respond to TextChanged. And I'd prefer the auto-scroll recalculation to be delayed so it's not firing dozens of times per second while a window is being resized.
For most UIs, text boxes with both vertical and horizontal scroll bars are, well, evil, so I'm only interested in vertical scroll bars here.
I also found that MeasureString produced a height that was actually bigger than what was required. Using the text box's PreferredHeight with no border as the line height gives a better result.
The following seems to work pretty well, with or without a border, and it works with WordWrap on.
Simply call AutoScrollVertically() when you need it, and optionally specify recalculateOnResize.
public class TextBoxAutoScroll : TextBox
{
public void AutoScrollVertically(bool recalculateOnResize = false)
{
SuspendLayout();
if (recalculateOnResize)
{
Resize -= OnResize;
Resize += OnResize;
}
float linesHeight = 0;
var borderStyle = BorderStyle;
BorderStyle = BorderStyle.None;
int textHeight = PreferredHeight;
try
{
using (var graphics = CreateGraphics())
{
foreach (var text in Lines)
{
var textArea = graphics.MeasureString(text, Font);
if (textArea.Width < Width)
linesHeight += textHeight;
else
{
var numLines = (float)Math.Ceiling(textArea.Width / Width);
linesHeight += textHeight * numLines;
}
}
}
if (linesHeight > Height)
ScrollBars = ScrollBars.Vertical;
else
ScrollBars = ScrollBars.None;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex);
}
finally
{
BorderStyle = borderStyle;
ResumeLayout();
}
}
private void OnResize(object sender, EventArgs e)
{
m_timerResize.Stop();
m_timerResize.Tick -= OnDelayedResize;
m_timerResize.Tick += OnDelayedResize;
m_timerResize.Interval = 475;
m_timerResize.Start();
}
Timer m_timerResize = new Timer();
private void OnDelayedResize(object sender, EventArgs e)
{
m_timerResize.Stop();
Resize -= OnResize;
AutoScrollVertically();
Resize += OnResize;
}
}

Resources