How to pass information from an attached behavior

2020-07-26 14:08发布

I have an attached property to textboxes in my view. The attached property performs validation on the textbox input and performs other chores. The attached property validation routine raises an event which is being watched by the viewmodel.

  1. Does this "violate" MVVM reasoning by having the viewmodel obtain the invalid TextBoxes?
  2. How will the GC deal with the static events from the attached property when the usercontrol containing the textboxes is removed?
  3. If specific code is needed to avoid memory leaks, how is that done?
  4. Is there a preferred way to do this?

Sorry for the long list, but Google does not address this situation.

Any and all help is appreciated. Thank you for your consideration.

(VS2010 .net 4.5)

TIA

ViewModel

class CheckInViewModel : SimpleViewModelBase
    {
        public CheckInViewModel()
        {
            InValidTextBoxes = new List<TextBox>();

            Stargate_V.Helpers.ColorMaskingTextBoxBehavior.Validated += (sender, e) =>
                {
                    if (e.valid)
                        InValidTextBoxes.Remove(e.sender);
                    else
                        InValidTextBoxes.Add(e.sender);
                };
        }

        List<TextBox> InValidTextBoxes;


    }

XAML

 <TextBox 
            h:ColorMaskingTextBoxBehavior.Mask="^[MmFf]$"
            Text="{Binding Sex}"
            Height="24" HorizontalAlignment="Right" Margin="0,55,665,0" VerticalAlignment ="Top" Width="36" />

Attached Properity

  public class ColorMaskingTextBoxBehavior : DependencyObject
    {
        // Entrance point from Xaml 
        public static readonly DependencyProperty MaskProperty = DependencyProperty.RegisterAttached("Mask",
                typeof(string),
                typeof(ColorMaskingTextBoxBehavior),
                new FrameworkPropertyMetadata(OnMaskChanged));
...........................

 // Callback from XAML initialization of the attached property.
    private static void OnMaskChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        var textBox = dependencyObject as TextBox;
        var mask = e.NewValue as string;
        textBox.PreviewTextInput -= textBox_PreviewTextInput;
        textBox.PreviewKeyDown -= textBox_PreviewKeyDown;
        DataObject.RemovePastingHandler(textBox, Pasting);
        DataObject.RemoveCopyingHandler(textBox, NoDragCopy);
        CommandManager.RemovePreviewExecutedHandler(textBox, NoCutting);


        if (mask == null)
        {
            textBox.ClearValue(MaskProperty);
            textBox.ClearValue(MaskExpressionProperty);
        }
        else
        {
            textBox.SetValue(MaskProperty, mask);
            SetMaskExpression(textBox, new Regex(mask, RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace));
            textBox.PreviewTextInput += textBox_PreviewTextInput;
            textBox.PreviewKeyDown += textBox_PreviewKeyDown;
            DataObject.AddPastingHandler(textBox, Pasting);
            DataObject.AddCopyingHandler(textBox, NoDragCopy);
            CommandManager.AddPreviewExecutedHandler(textBox, NoCutting);
        }
    }



private static void textBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
        {
            var textBox = sender as TextBox;
            var maskExpression = GetMaskExpression(textBox);

            string passHex = (string)textBox.GetValue(PassColorProperty);
            string failHex = (string)textBox.GetValue(FailColorProperty);
            Color passColor = Extensions.ToColorFromHex(passHex);
            Color failColor = Extensions.ToColorFromHex(failHex);

            if (maskExpression == null)
            {
                return;
            }

            var proposedText = GetProposedText(textBox, e.Text);

            if (!maskExpression.IsMatch(proposedText))
            {
                textBox.Background = new SolidColorBrush(failColor);

                ValidationEventArgs args = new ValidationEventArgs();
                args.sender = textBox;
                args.valid = false;
                OnValidation(args);
            }
            else
            {
                textBox.Background = new SolidColorBrush(passColor);

                ValidationEventArgs args = new ValidationEventArgs();
                args.sender = textBox;
                args.valid = true;
                OnValidation(args);
            }
        }

Event Called from the above code

    public static event EventHandler<ValidationEventArgs> Validated;

    static void OnValidation(ValidationEventArgs e)
    {
        EventHandler<ValidationEventArgs> handler = Validated;
        if (handler != null)
        {
            handler(null, e);
        }
    }


public class ValidationEventArgs : EventArgs
{
    public TextBox sender;
    public bool valid;
}

标签: c# wpf mvvm
1条回答
三岁会撩人
2楼-- · 2020-07-26 14:40

Yes, I would argue that this violates MVVM. Your view model should have no knowledge of the views whatsoever. The question to always ask yourself is "can I run my application without creating any views?". In this case your view model is interacting directly with a list of TextBoxes, so the pattern is broken.

There are several ways of achieving your goal here, probably the most simple is to create a handler in your view model that gets called when your TextBox text changes:

public delegate void ValidationDelegate(bool isValid);

public class MyViewModel : ViewModelBase
{
    public ValidationDelegate ValidationHandler { get { return (isValid) => OnValidate(isValid); } }

    private void OnValidate(bool isValid)
    {
        // handle the validation event here
    }
}

Now all you need is a behavior with an attached property that you can bind to this handler:

public class ValidateBehavior : Behavior<TextBox>
{
    public ValidationDelegate Validated
    {
        get { return (ValidationDelegate)GetValue(ValidatedProperty); }
        set { SetValue(ValidatedProperty, value); }
    }

    public static readonly DependencyProperty ValidatedProperty =
        DependencyProperty.Register("Validated", typeof(ValidationDelegate), typeof(ValidateBehavior), new PropertyMetadata(null));

    protected override void OnAttached()
    {
        base.OnAttached();
        this.AssociatedObject.TextChanged += ValidateText;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        this.AssociatedObject.TextChanged -= ValidateText;
    }

    private void ValidateText(object sender, TextChangedEventArgs e)
    {
        if (this.Validated != null)
        {
            bool isValid = true; // do text validation here
            this.Validated(isValid);
        }
    }
}

And then finally add the behaviour to the TextBox in question and bind the handler:

    <TextBox>
        <i:Interaction.Behaviors>
            <behaviors:ValidateBehavior Validated="{Binding ValidationHandler}"/>
        </i:Interaction.Behaviors>
    </TextBox>

EDIT: If you don't want to use a Blend behaviour then you can also do it with an attached behaviour:

public static class ValidateBehavior
{
    public static ValidationDelegate GetValidate(TextBox textbox)
    {
        return (ValidationDelegate)textbox.GetValue(ValidateProperty);
    }

    public static void SetValidate(TextBox textbox, ValidationDelegate value)
    {
        textbox.SetValue(ValidateProperty, value);
    }

    public static readonly DependencyProperty ValidateProperty =
        DependencyProperty.RegisterAttached(
        "Validate",
        typeof(ValidationDelegate),
        typeof(ValidateBehavior),
        new UIPropertyMetadata(null, OnValidateChanged));

    static void OnValidateChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
    {
        var textbox = depObj as TextBox;
        if (textbox == null)
            return;

        if (e.OldValue is ValidationDelegate)
            textbox.TextChanged -= OnTextChanged;

        if (e.NewValue is ValidationDelegate)
            textbox.TextChanged += OnTextChanged;
    }

    static void OnTextChanged(object sender, RoutedEventArgs e)
    {
        if (!Object.ReferenceEquals(sender, e.OriginalSource))
            return;

        var textbox = e.OriginalSource as TextBox;
        if (textbox != null)
        {
            var validate = GetValidate(textbox);
            if (validate != null)
            {
                bool isValid = true; // do text validation here
                validate(isValid);
            }
        }
    }
}

And the corresponding XAML:

<TextBox behaviors:ValidateBehavior.Validate="{Binding ValidationHandler}" />
查看更多
登录 后发表回答