1

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, "");
    }
}
}
H.B.
  • 166,899
  • 29
  • 327
  • 400
Dave
  • 8,095
  • 14
  • 56
  • 99
  • I don't know if this may relate to you but I found this page - http://stackoverflow.com/questions/3120463/databinding-to-validation-haserror . It seems like there may be a problem binding to Validation.HasError , Although in some other examples it seems like it's possible - http://stackoverflow.com/questions/4574304/wpf-validation-how-to-show-tooltips-and-disable-run-button – Dror Feb 15 '12 at 06:14
  • @Dave Have you tries ViewModel approach instead of using triggers? – Ankesh Feb 15 '12 at 07:33
  • Thanks Orchestrator for the links. You'll notice in the second link that the poster uses Validation.HasError but not with the concept of MultiBinding Converter. I think I need that because the condition to enable the button is not trivial. It's a "if (A AND B) OR (C AND D). MultiDataTrigger.Conditions on their own don't seem to lend themselves to this. He also introduces yet another data validation technique, IDataErrorInfo. Not sure if this is the way to go or not. Too many choices! – Dave Feb 15 '12 at 18:16
  • adcool2007, Could you elaborate or post a link to ViewModel with triggers? Thanks, Dave – Dave Feb 15 '12 at 18:17

1 Answers1

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();
}
AkselK
  • 2,563
  • 21
  • 39
  • I coded up your suggestion, and it looks promissing except for the part you marked (...). How does one tell that the editboxes are in an error state? Recall, as I have it coded now, you can't just look at the property backing the editboxes. For better or worse, I'm using ValidationRules for each editbox. It will be highlighted in Red, but the property won't be written to yet. I need something like textBox8DigitId.HasErrors but I don't see a property like that. Ideas? – Dave Feb 15 '12 at 20:54
  • Ahh got it (or very close) I added: bool result = (Grp8Digit.Visibility == Visibility.Visible) && !Validation.GetHasError(m_textBox8DigitId) &&m_textBox8DigitId.Text.Length == 8; e.CanExecute = result; The key is the Validation.GetHasError. – Dave Feb 15 '12 at 21:45
  • However, notice I also had to add the m_textBox8DigitId.Text.Length == 8. This was for the startup case where on startup this method gets called and for whatever reason, Validation.GetHasError(...) returns false. My solution is less than ideas because in my real project, it's not as simple as just having 8 characters. Ideas? Is there a way to set the edit box as invalid at startup? – Dave Feb 15 '12 at 21:48
  • Validation only update when the binding updates. For a simple workaround, see edited answer. – AkselK Feb 16 '12 at 08:08
  • Thanks AkselK. Marked as answer with many thanks. For those reading this in the future, I also added around the textboxes. This was because the red error outline of the first edit box would remain when I toggled the rado button to go to the second edit box. See for example: http://stackoverflow.com/questions/1471451/wpf-error-template-red-box-still-visible-on-collapse-of-an-expander/1471733#1471733 – Dave Feb 16 '12 at 18:26