Manuel
Manuel

Reputation: 11489

How to know when textbox is being modified?

I'm using the Delay binding tag of .Net 4.5 but I want to change the textbox's background color while the changes are not "committed". How can I set an IsDirty property to true while the delay is happening?

I tried using the TextChanged event to set an IsDirty flag and then remove the flag when the bound property got set. The problem is that the TextChanged fires whenever the bound property changes and not just when the user modifies the text.

I got it "working" in a very clunky and fragile way by monitoring the TextChanged event and the bound property. Needless to say this is very prone to bugs so I would like a cleaner solution. Is there any way to know that the textbox has been changed but not committed yet (by the Delay)?

Upvotes: 4

Views: 2923

Answers (3)

Fredrik Hedblad
Fredrik Hedblad

Reputation: 84684

I had a look through the source code and the BindingExpressionBase itself is aware of this through a property called NeedsUpdate. But this property is internal so you would have to use reflection to get it.

However, you won't be able to monitor this property in any easy way. So the way I see it, you would need to use both of the events TextChanged and SourceUpdated to know when NeedsUpdate might have changed.

Update
I created an attached behavior that does this, it can be used to monitor pending updates on any DependencyProperty. Note that NotifyOnSourceUpdated must be set to true.

Uploaded a small sample project here: PendingUpdateExample.zip

Example

<TextBox Text="{Binding ElementName=textBoxSource,
                        Path=Text,
                        NotifyOnSourceUpdated=True,
                        UpdateSourceTrigger=PropertyChanged,
                        Delay=1000}"
         ab:UpdatePendingBehavior.MonitorPendingUpdates="{x:Static TextBox.TextProperty}">
    <TextBox.Style>
        <Style TargetType="TextBox">
            <Style.Triggers>
                <Trigger Property="ab:UpdatePendingBehavior.HasPendingUpdates"
                         Value="True">
                    <Setter Property="Background" Value="Green"/>
                </Trigger>
            </Style.Triggers>
        </Style>
    </TextBox.Style>
</TextBox>

UpdatePendingBehavior

public class UpdatePendingBehavior
{
    #region MonitorPendingUpdates

    public static DependencyProperty MonitorPendingUpdatesProperty =
        DependencyProperty.RegisterAttached("MonitorPendingUpdates",
                                            typeof(DependencyProperty),
                                            typeof(UpdatePendingBehavior),
                                            new UIPropertyMetadata(null, MonitorPendingUpdatesChanged));

    public static DependencyProperty GetMonitorPendingUpdates(FrameworkElement obj)
    {
        return (DependencyProperty)obj.GetValue(MonitorPendingUpdatesProperty);
    }
    public static void SetMonitorPendingUpdates(FrameworkElement obj, DependencyProperty value)
    {
        obj.SetValue(MonitorPendingUpdatesProperty, value);
    }

    public static void MonitorPendingUpdatesChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
    {
        DependencyProperty property = e.NewValue as DependencyProperty;
        if (property != null)
        {
            FrameworkElement element = target as FrameworkElement;
            element.SourceUpdated += elementProperty_SourceUpdated;

            if (element.IsLoaded == true)
            {
                SubscribeToChanges(element, property);
            }
            element.Loaded += delegate { SubscribeToChanges(element, property); };
            element.Unloaded += delegate { UnsubscribeToChanges(element, property); };
        }
    }

    private static void SubscribeToChanges(FrameworkElement element, DependencyProperty property)
    {
        DependencyPropertyDescriptor propertyDescriptor =
            DependencyPropertyDescriptor.FromProperty(property, element.GetType());
        propertyDescriptor.AddValueChanged(element, elementProperty_TargetUpdated);
    }

    private static void UnsubscribeToChanges(FrameworkElement element, DependencyProperty property)
    {
        DependencyPropertyDescriptor propertyDescriptor =
                DependencyPropertyDescriptor.FromProperty(property, element.GetType());
        propertyDescriptor.RemoveValueChanged(element, elementProperty_TargetUpdated);
    }

    private static void elementProperty_TargetUpdated(object sender, EventArgs e)
    {
        FrameworkElement element = sender as FrameworkElement;
        UpdatePendingChanges(element);
    }

    private static void elementProperty_SourceUpdated(object sender, DataTransferEventArgs e)
    {
        FrameworkElement element = sender as FrameworkElement;
        if (e.Property == GetMonitorPendingUpdates(element))
        {
            UpdatePendingChanges(element);
        }
    }

    private static void UpdatePendingChanges(FrameworkElement element)
    {
        BindingExpressionBase beb = BindingOperations.GetBindingExpressionBase(element, GetMonitorPendingUpdates(element));
        BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic;
        PropertyInfo needsUpdateProperty = beb.GetType().GetProperty("NeedsUpdate", bindingFlags);
        SetHasPendingUpdates(element, (bool)needsUpdateProperty.GetValue(beb));
    }

    #endregion // MonitorPendingUpdates

    #region HasPendingUpdates

    public static DependencyProperty HasPendingUpdatesProperty =
        DependencyProperty.RegisterAttached("HasPendingUpdates",
                                            typeof(bool),
                                            typeof(UpdatePendingBehavior),
                                            new UIPropertyMetadata(false));

    public static bool GetHasPendingUpdates(FrameworkElement obj)
    {
        return (bool)obj.GetValue(HasPendingUpdatesProperty);
    }
    public static void SetHasPendingUpdates(FrameworkElement obj, bool value)
    {
        obj.SetValue(HasPendingUpdatesProperty, value);
    }

    #endregion // HasPendingUpdates
}

Another way could be to use a MultiBinding that binds both to the source and the target and compares their values in a converter. Then you could change the Background in the Style. This assumes that you don't convert the value. Example with two TextBoxes

<TextBox Text="{Binding ElementName=textBoxSource,
                        Path=Text,
                        UpdateSourceTrigger=PropertyChanged,
                        Delay=2000}">
    <TextBox.Style>
        <Style TargetType="TextBox">
            <Style.Triggers>
                <DataTrigger Value="False">
                    <DataTrigger.Binding>
                        <MultiBinding Converter="{StaticResource IsTextEqualConverter}">
                            <Binding RelativeSource="{RelativeSource Self}"
                                     Path="Text"/>
                            <Binding ElementName="textBoxSource" Path="Text"/>
                        </MultiBinding>
                    </DataTrigger.Binding>
                    <Setter Property="Background" Value="Green"/>
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </TextBox.Style>
</TextBox>
<TextBox Name="textBoxSource"/>

IsTextEqualConverter

public class IsTextEqualConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        return values[0].ToString() == values[1].ToString();
    }
    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}

Upvotes: 6

hbarck
hbarck

Reputation: 2944

I'm not sure about the Delay binding. However, in .Net 4.0 I'd use a BindingGroup. BindingGroup has a CanRestoreValues property which will report exactly what you want: If the UpdateSourceTrigger is Explicit (which is default if there is a BindingGroup), CanRestoreValues will be true from the time one bound control value has been changed until BindingGroup.CommitEdit is called and the values are forwarded to the bound object. However, CanRestoreValues will be true as soon as any control that shares the BindingGroup has a pending value, so if you want feedback for each separate property you will have to use one BindingGroup per control, which makes calling CommitEdit a little bit less convenient.

Upvotes: 0

brunnerh
brunnerh

Reputation: 185553

You could try some other event like PreviewTextInput or one of the key related ones. (You probably need the tunneling versions as bubbling events are probably handled internally)

Upvotes: 0

Related Questions