views:

124

answers:

3

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 Repositorythat is currently being displayed; the SelectionStart, 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.

A: 

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

Leigh Shayler
The textbox is always focused.
Justin
+1  A: 

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!

Steve Greatrex
I don't see how I can fix this. Could you go into more detail? Do you need more info from me?
Justin
I'm not sure that it can be "fixed" exactly, as its behaving as you would expect. What I meant was that there may be another way to achieve your desired results - what is it that you want to do with the selection info?
Steve Greatrex
I want it to be able to restore the selection info, when I switch between repositories.
Justin
A: 

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.

Akash Kava