Disabling button based on multiple properties. I&#

2019-07-28 19:57发布

问题:

I have placed below the simplest example I could come up with to demonstrate my problem. I am trying to to enable a button based on 1 of 2 conditions 1) Textbox1 is visible AND contents are valid or 2) Textbox2 is visible AND contents are valid

I seem to be on my way to enabling the button based on visibility, but the IsValid aspect is giving me grief.

For the button, I have introduced a MultiDataTrigger and MultiBinding with a MultiBinding Converter method to evaluate whether the button should be enabled or not. The method (called myConverter) is called when I switch between editboxes (by clicking a radio button), but does not seem to be called when the data in the edit box is valid, invalid, or transitions between the two. Quite possibly, I'm not correctly handling Validation.HasError

My specific questions: 1) What's the correct pattern to handle this problem? Any examples? I should say that I've simplified the problem. For example, the validation might be more than just "must be eight characters", and there could be multiple edit boxes involved (like "address" and "zip" OR "address" and "state". Thus I think I probably need the MultiBinding Converter idea but I'm open to other ideas! 2) How do I handle Validation.HasError inside my Converter method? I'm treating it as ReadOnlyCollection which is probably totally wrong! 3) I think a large part of my problems are due to the many choices to handle error info. Given that I'm using ValidationRules, should I also be throwing Exceptions from my properties that back the edit fields? Will they ever be called? Can you recommend an article showing the different ways to do validation?

I've put ALL the code below. I'd be most appreciative if someone could take a quick look and point me in the right direction. -Dave XAML code

<Window x:Class="StackOverFlowBindingExample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:StackOverFlowBindingExample"
Title="Window1" Height="Auto" MinWidth="500" SizeToContent="Manual" WindowStartupLocation="CenterOwner" ResizeMode="CanResizeWithGrip" >
<Window.Resources>
    <local:MyConverter x:Key="myConverter" />
    <Style x:Key="textStyleTextBox" TargetType="TextBox">
        <Setter Property="Foreground" Value="#333333" />
        <Setter Property="VerticalAlignment" Value="Top" />
        <Setter Property="MinHeight" Value="2" />
        <Setter Property="MinWidth" Value="100" />
        <Setter Property="Margin" Value="4" />
        <Setter Property="MaxLength" Value="23" />
        <Setter Property="VerticalContentAlignment" Value="Center" />
        <Setter Property="HorizontalAlignment" Value="Left" />


        <Style.Triggers>
            <Trigger Property="Validation.HasError" Value="true">
                <Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self},
                    Path=(Validation.Errors)[0].ErrorContent}" />
            </Trigger>
        </Style.Triggers>
    </Style>



</Window.Resources>
<Grid>
    <StackPanel Orientation="Vertical">
        <StackPanel Orientation="Horizontal">
            <StackPanel Orientation="Vertical">
                <RadioButton Name="m_radio1" Margin="4" GroupName="IdInputType" IsChecked="True" Checked="IdInputType_Changed">Use Inputtype1</RadioButton>
                <RadioButton Name="m_radio2" Margin="4" GroupName="IdInputType" IsChecked="False" Checked="IdInputType_Changed">Use Inputtype2</RadioButton>
            </StackPanel>
            <DockPanel Name="Grp8Digit">
                <Label MinHeight="25" Margin="4" VerticalAlignment="Top" VerticalContentAlignment="Center" HorizontalAlignment="Left" MinWidth="100" Width="113">Type 1 Id:</Label>
                <TextBox Height="23" Name="m_textBox8DigitId" MaxLength="8" Width="120" Style="{StaticResource textStyleTextBox}" Validation.Error="TextBox_Error">
                    <TextBox.Text>
                        <Binding>
                            <Binding.ValidatesOnExceptions>true</Binding.ValidatesOnExceptions>
                            <Binding.ValidatesOnDataErrors>true</Binding.ValidatesOnDataErrors>
    <Binding.UpdateSourceTrigger>PropertyChanged</Binding.UpdateSourceTrigger>
                            <Binding.Path>EightDigitId</Binding.Path>
                            <Binding.NotifyOnValidationError>true</Binding.NotifyOnValidationError>
                            <Binding.ValidationRules>
                                <local:EightByteStringConvertRule />
                            </Binding.ValidationRules>
                        </Binding>
                    </TextBox.Text>
                </TextBox>
                <Label MinHeight="25" Margin="4" VerticalAlignment="Top" VerticalContentAlignment="Center" HorizontalAlignment="Left" MinWidth="100">Enter 8 digit id</Label>
            </DockPanel>
            <DockPanel Name="Grp5Digit" Visibility="Collapsed">


                <StackPanel Orientation="Horizontal">
                    <Label MinHeight="25" Margin="4" VerticalAlignment="Top" VerticalContentAlignment="Center" HorizontalAlignment="Left" MinWidth="100" Width="113">Type 2 id:</Label>

                    <TextBox Name="m_textBox5DigitId" Style="{StaticResource textStyleTextBox}" MinHeight="25" Margin="4" VerticalAlignment="Top" MaxLength="23" VerticalContentAlignment="Center" HorizontalAlignment="Left" MinWidth="100" Width="100" ToolTip="Enter Type 2 id">
                        <TextBox.Text>
                            <Binding>
                                <Binding.ValidatesOnExceptions>true</Binding.ValidatesOnExceptions>
                                <Binding.Path>FiveDigitId</Binding.Path>

                                <Binding.NotifyOnValidationError>true</Binding.NotifyOnValidationError>
                                <Binding.ValidationRules>
                                    <local:FiveByteStringConvertRule />
                                </Binding.ValidationRules>
                            </Binding>
                        </TextBox.Text>
                    </TextBox>
                    <Label MinHeight="25" Margin="4" VerticalAlignment="Top" VerticalContentAlignment="Center" HorizontalAlignment="Left" MinWidth="100">Enter 5 digit id</Label>

                </StackPanel>
            </DockPanel>
        </StackPanel>

        <Button Height="27" Name="btnDoSomething" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="4" HorizontalContentAlignment="Center" Click="btnDoSomething_Click" Content="Do Something">
            <Button.Style>
                <Style TargetType="{x:Type Button}">
                    <Setter Property="IsEnabled" Value="false" />
                    <Style.Triggers>
                        <MultiDataTrigger>
                            <MultiDataTrigger.Conditions>
                                <Condition Value="true">
                                    <Condition.Binding>
                                        <MultiBinding Converter="{StaticResource myConverter}">
                                            <Binding ElementName="Grp8Digit" Path="Visibility" />
                                            <Binding ElementName="m_textBox8DigitId" Path="Validation.HasError" />
                                            <Binding ElementName="Grp5Digit" Path="Visibility" />
                                            <Binding ElementName="m_textBox5DigitId" Path="Validation.HasError" />


                                        </MultiBinding>
                                    </Condition.Binding>
                                </Condition>
                            </MultiDataTrigger.Conditions>
                            <Setter Property="IsEnabled" Value="true" />
                        </MultiDataTrigger>
                    </Style.Triggers>
                </Style>
            </Button.Style>
        </Button>
    </StackPanel>
</Grid>

C# code

using System;
// lots of usings!!!
namespace StackOverFlowBindingExample
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window, INotifyPropertyChanged
{
    private static readonly object eightDigitLock = new object();
    private string _eightdigitId;
    public string EightDigitId
    {
        get
        {
            return _eightdigitId;
        }
        set
        {
            lock (eightDigitLock)
            {
                if (value != _eightdigitId)
                {
                    if (value.Length == 8)
                        _eightdigitId = value;
                    else
                        throw new Exception("Must be 8 digits");// do I really need to throw Exception here?

                }
            }
        }
    }

    private static readonly object fiveDigitLock = new object();
    private string _fivedigitId;
    public string FiveDigitId
    {
        get
        {
            return _fivedigitId;
        }
        set
        {
            lock (fiveDigitLock)
            {
                if (value != _fivedigitId)
                {
                    if (value.Length == 5)
                        _fivedigitId = value;
                    else
                        throw new Exception("Must be 5 digits");// do I really need to throw exception?

                }
            }
        }
    }
    public Window1()
    {
        InitializeComponent();
        this.DataContext = this;
    }
    private void IdInputType_Changed(object sender, RoutedEventArgs e)
    {
        if (m_radio1 != null && Grp8Digit != null && Grp5Digit != null)
        {
            if (m_radio1.IsChecked == true)
            {
                Grp8Digit.Visibility = Visibility.Visible;
                Grp5Digit.Visibility = Visibility.Collapsed;

            }
            else
            {
                Grp8Digit.Visibility = Visibility.Collapsed;
                Grp5Digit.Visibility = Visibility.Visible;

            }
        }

    }

    private void TextBox_Error(object sender, ValidationErrorEventArgs e)
    {
        try
        {
            if (e.Action == ValidationErrorEventAction.Added)
            {
                try
                {
                    if (e.Error.Exception != null && e.Error.Exception.InnerException != null && e.Error.Exception.InnerException.Message.Length > 0)
                    {
                        ((Control)sender).ToolTip = e.Error.Exception.InnerException.Message;
                    }
                    else
                    {
                        ((Control)sender).ToolTip = e.Error.ErrorContent.ToString();
                    }
                }
                catch (Exception ex)
                {
                    string msg = ex.Message;
                    //Common.ProgramContext.Current.AddSessionLogEntrySync(new LogEntry(LogEntryCategory.Exception, ex.ToString()));
                    ((Control)sender).ToolTip = e.Error.ErrorContent.ToString();
                }
            }
            else
            {
                ((Control)sender).ToolTip = "";
            }
        }
        catch (Exception)
        {
            //Common.ProgramContext.Current.AddSessionLogEntrySync(new LogEntry(LogEntryCategory.Exception, ex.ToString()));
            ((Control)sender).ToolTip = "";
        }
    }

    private void btnDoSomething_Click(object sender, RoutedEventArgs e)
    {

    }
    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged(string name)
    {

        PropertyChangedEventHandler handler = PropertyChanged;

        if (handler != null)
        {

            handler(this, new PropertyChangedEventArgs(name));

        }

    }
    #endregion
}

public class MyConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter,
        System.Globalization.CultureInfo culture)
    {

        bool valid8Digit = true;
        ReadOnlyCollection<ValidationError> collection = values[1] as ReadOnlyCollection<ValidationError>;
        if (collection != null && collection.Count > 0)
        {
            valid8Digit = false;

        }
        //if ((bool)values[0] == true)//&& (bool)values[1] == false)
        if ((Visibility)values[0] == Visibility.Visible && valid8Digit)
        {

            return true;
        }
        else
            return false;


    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter,
        System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

public class FiveByteStringConvertRule   : ValidationRule
{

    public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
    {
        if ((value as string) != null && (value as string).Length == 5)
            return new ValidationResult(true, "");
        else
            return new ValidationResult(false, "");
    }
}

public class EightByteStringConvertRule : ValidationRule
{

    public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
    {
        if ((value as string) != null && (value as string).Length == 8)
            return new ValidationResult(true, "");
        else
            return new ValidationResult(false, "");
    }
}
}

回答1:

You should use commands to disable/enable buttons. It's the easiest and cleanest way of doing what you wish to do.

In your code-behind file, declare a new static class, Commands, and declare a new RoutedUICommand.

public static class Commands
{
    public static readonly RoutedUICommand DoSomething = new RoutedUICommand("Do Something", "DoSomething", typeof(MainWindow)); // MainWindow or the name of the usercontrol where you are going to use it.
}

To use this, you need to declare a CommandBinding in your Window/UserControl.

<Window.CommandBindings>
    <CommandBinding Command="my:Commands.DoSomething" CanExecute="DoSomethingCanExecute" Executed="DoSomethingExecuted" />
</Window.CommandBindings>

my: is my local namespace.

Then you can simply set the button to use that command.

<Button Command="my:Commmands.DoSomething"/>

The CommandBinding's CanExecute and Executed events are where your logic should lie. To disable/enable the button, simply handle that in DoSomethingCanExecute.

private void ShowXRefExecuted(object sender, ExecutedRoutedEventArgs e)
    {
        e.CanExecute = (Grp8Digit.Visibility==Visibility.Visible && (...) );

    }

And of course, the Executed event is what happens when the user clicks the button.

EDIT

The validation event only triggers when bindings update. To force a validation, you could update the triggers by hand as soon as the window/usercontrol has loaded. In the Window's Loaded event:

public void Window_Loaded(object sender, RoutedEventArgs e)
{
    m_textBox8DigitId.GetBindingExpression(TextBox.TextProperty).UpdateSource();
}