I am working on an application where Repository
objects are displayed via a DataTemplate that contains a modified version of a TextBox
, which supports binding to the SelectionStart
, SelectionLength
, and VerticalOffset
.
The DataTemplate looks like this:
<DataTemplate DataType="{x:Type m:Repository}">
<controls:ModdedTextBox
x:Name="textBox" Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}"
BindableSelectionStart="{Binding SelectionStart, UpdateSourceTrigger=PropertyChanged}"
BindableSelectionLength="{Binding SelectionLength, UpdateSourceTrigger=PropertyChanged}"
BindableVerticalOffset="{Binding VerticalOffset, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
The problem is that when I change the Repository
that is currently being displayed; the SelectionStart
开发者_Go百科, SelectionLength
, and VerticalOffset
all seem to be getting set to 0, even when those properties of the Repository
object are not 0.
I think that this is happening in the instant before the text is displayed when the SelectionStart
, SelectionLength
, and VerticalOffset
can not be more than 0. This does not only set the actual properties of the TextBox
to zero, but also updates the bindings and sets the properties of the Repository
object to zero.
Is there any way that I can prevent this from happening?
--Edit--
I don't know if posting dl links to projects is a no-no or not on SO, but here is a link to a project I created to demonstrate the problem I am having: http://dl.dropbox.com/u/1520079/RepositoryProblemDemo.zip
When you run the demo-app the selection you can click the "Switch Repository" button to change the repository that is displayed in the textbox. If you look to the right of the textbox the current repository's properties all get set to zero when you switch to the other one.
A difference between this demo and my actual app is that in my app repositories will be switched via hotkeys, not a button.
The problem is due to the fact that the bindings are evaluated in serial, and when the Text
property is changed it causes all selection information to be removed (you can see this by putting breakpoints on your ModdedTextBox
event handlers). As the BindableSelection... bindings are still active at that point, it causes the selection information to be reset.
Depending on the exact behaviour you want there is probably a way to work around this, but you would need to know a little more detail...
Edit in response to comments: This solution isn't exactly answering your original question, and it probably isn't great practice, but it does at least work...
Try altering your ModdedTextBox so that instead of exposing bindable properties for the selection information, expose a single DP of type Repository and bind to that:
<local:ModdedTextBox
x:Name="textBox"
Repository="{Binding CurrentRepository}"
TextWrapping="Wrap"
/>
Then handle the changed event on your DP to set the text box properties:
public static DependencyProperty RepositoryProperty =
DependencyProperty.Register("Repository",
typeof(Repository), typeof(ModdedTextBox), new PropertyMetadata(null, OnRepositoryChanged));
public Repository Repository
{
get { return (Repository)base.GetValue(RepositoryProperty); }
set { base.SetValue(RepositoryProperty, value); }
}
private static void OnRepositoryChanged(DependencyObject senderObject, DependencyPropertyChangedEventArgs e)
{
var sender = (ModdedTextBox)senderObject;
var oldRepository = e.OldValue as Repository;
var newRepository = e.NewValue as Repository;
if (oldRepository != null)
{
oldRepository.Text = sender.Text;
oldRepository.SelectionStart = sender.SelectionStart;
//etc
}
if (newRepository != null)
{
sender.Text = newRepository.Text;
sender.SelectionStart = newRepository.SelectionStart;
//etc
}
}
This is essentially removing the serial nature of the binding evaluation.
Note: You could also achieve the same using attached properties, which would be better than subclassing TextBox, but this is closer to your original attempts so I figure its easier to explain!
Here is a re-write of the other solution. This one takes into account the text property not being bound before the other properties.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
namespace WpfApplication1
{
public class SelectionBindingTextBox : TextBox
{
public static readonly DependencyProperty BindableSelectionStartProperty =
DependencyProperty.Register(
"BindableSelectionStart",
typeof(int),
typeof(SelectionBindingTextBox),
new PropertyMetadata(OnBindableSelectionStartChanged));
public static readonly DependencyProperty BindableSelectionLengthProperty =
DependencyProperty.Register(
"BindableSelectionLength",
typeof(int),
typeof(SelectionBindingTextBox),
new PropertyMetadata(OnBindableSelectionLengthChanged));
private bool isBindingComplete = false;
public SelectionBindingTextBox()
: base()
{
this.SelectionChanged += this.OnSelectionChanged;
this.TextChanged += this.OnTextChanged;
}
public int BindableSelectionStart
{
get
{
return (int)this.GetValue(BindableSelectionStartProperty);
}
set
{
this.SetValue(BindableSelectionStartProperty, value);
}
}
public int BindableSelectionLength
{
get
{
return (int)this.GetValue(BindableSelectionLengthProperty);
}
set
{
this.SetValue(BindableSelectionLengthProperty, value);
}
}
private static void OnBindableSelectionStartChanged(DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs args)
{
var textBox = dependencyObject as SelectionBindingTextBox;
if (textBox.isBindingComplete)
{
textBox.SetupSelection();
}
}
private static void OnBindableSelectionLengthChanged(DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs args)
{
var textBox = dependencyObject as SelectionBindingTextBox;
if (textBox.isBindingComplete)
{
textBox.SetupSelection();
}
}
private void OnSelectionChanged(object sender, RoutedEventArgs e)
{
if (isBindingComplete)
{
this.BindableSelectionStart = this.SelectionStart;
this.BindableSelectionLength = this.SelectionLength;
}
}
private void OnTextChanged(object sender, RoutedEventArgs e)
{
if (!isBindingComplete)
{
SetupSelection();
}
isBindingComplete = true;
}
private void SetupSelection()
{
// this.Focus();
this.SelectionLength = this.BindableSelectionLength;
this.SelectionStart = this.BindableSelectionStart;
}
}
}
Well update of binding depends on the order in which WPF or Silverlight Engine will evaluate, looks like your SelectionStart and SelectionEnd bindings are updated before Text, so when Text gets changed, SelectionStart and SelectionEnd are both changed back to zero.
The only way is to hook for TextChanged event and refresh the bindings of SelectionStart and SelectionEnd or in WPF you can extend textbox as follow
public class MyTextBox : TextBox{
protected override OnTextChanged(TextChangedEventArgs e){
BindingExpression be = this.GetBindingExpression(SelectionStartProperty);
if(be!=null){
be.UpdateTarget();
}
be = this.GetBindingExpression(SelectionEndProperty);
if(be!=null){
be.UpdateTarget();
}
be = this.GetBindingExpression(VerticalOffsetProperty);
if(be!=null){
be.UpdateTarget();
}
}
}
Well here there is a trick, you still have to change above logic to fit in your logic because everytime text updates this will update binding, so you have to find out when to refresh these bindings. Because this will consistantly fail to change your textbox's value in runtime as text will modify and selection will goto previous selection only.
精彩评论