简体   繁体   中英

Bound Button.IsEnabled() Property doesn't work properly

I'm new with MVVM Pattern.

I was wondering why everytime my TextChanged() event is fired, the binded IsEnabled() property doesn't change its state. The TextChanged() event is calling IsValid() to check for data validation.

I have this simple ViewModel class

public class myViewModel : ViewModel
{
    public bool IsOk { get; set; }
    public RelayCommand OnValidateExecute { get; set; }

    public myViewModel()
    {
        OnValidateExecute = new RelayCommand(p => IsValid(), p => true);
    }

    public bool IsValid()
    {
        // other codes
        IsOk = MethodName();
        NotifyPropertyChanged("IsOk");
    }
}

I set a breakpoint on IsValid() and the code is working fine. I was wondering why IsEnabled property is not working as expected.

This is my XAML code

<TextBox ...other propeties here....>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="TextChanged">
            <i:InvokeCommandAction Command="{Binding OnValidateExecute}">
            </i:InvokeCommandAction>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</TextBox>

<Button x:Name="btnSave" IsEnabled="{Binding IsOk, Mode=TwoWay}" 
            ...other propeties here....>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
            <i:InvokeCommandAction Command="{Binding OnSaveExecute}">
            </i:InvokeCommandAction>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>

What I want is that when IsOk property is false, the button should be Disabled , otherwise, Enabled .

Is there some wrong with my data binding? If this question has been asked before, kindly help me redirect on to it.

UPDATE 1

Another problem I have encounter with this code is that, the IsValid() function is triggered first before the setting of value on the textbox. Here's an example, Assuming that the starting value of the textbox is 0 , when I changed it to, let's say, 9, the value that will be check is the previous value 0 instead of 9 . Any idea why this is happening? Is there a problem with the binding?

Here is the answer, and include my MVVM Framework's important part. Beside this I put some extra function on it . I can't put here all of my library. But I'm sure it will help.

If you use Commanding you should be aware of CanExecuteChanged in ICommand interface. You should trigger this command when a property changed. (I don't use RelayCommand, it is 3.party .)

Use my DCommand :) this is the most important part

It has easy implementation , and has FirePropertyChanged method. This method fires CanExecuteChanged of ICommand if it is not null.

An example Command

Anonymous syntax

   DCommand commandPost=new DCommand(()=>{
      //TODO:Command's real execute method Post()
        },
      ()=>
      {
         return this.TextBoxBoundProperty.IsValid;
      }
    )

Non-anonymous syntax

    DCommand commandPost=(Post,Validate);

beside this you should trigger canexecutechanged by following method in your viewModel's ctor.

    this.PropertyChanged += (sender, prop) =>
          {
        //I preffered canExcuteChange for any property changes for my viewmodel class. You could put your own logic. if(prop.PropertyName.equals("thisone"));
        //Just works for this class's property changed
                this.InvokeOnClassPropertyChange(prop.PropertyName, () =>
                {
                    this.commandPost.FirePropertyChanged();
                });
          }

InvokeOnClassPropertyChange works when if the property is ViewModel class's property.

    public static void InvokeOnClassPropertyChange(this object instance,string PropertyName,Action action)
    {
        Type type = instance.GetType();
        var fulllist = type.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(w => w.DeclaringType == type).ToList();
        if (fulllist.Select(p => p.Name).Contains(PropertyName))
        {
            action.Invoke();
        }
    }

The above code shows InvokeOnClassPropertyChange extension method. The below one shows my DCommand implements ICommand.

   public class DCommand :ICommand
    {
    public void FirePropertyChanged()
    {
        if (CanExecuteChanged!=null)
        CanExecuteChanged(this, EventArgs.Empty);            
    }

    Func<bool> CanExecuteFunc { get; set; }
    Action<object> Action { get; set; }


    public DCommand(Action<object> executeMethod)
    {
        this.Action = executeMethod;            
    }

    public DCommand(Action executeMethod)
    {
        this.Action = new Action<object>(
            (prm) =>
            {
                executeMethod.Invoke();
            }
            );
    }

    public DCommand(Action<object> executeMethod, Func<bool> canExecuteMethod)
        : this(executeMethod)
    {            
        this.CanExecuteFunc = canExecuteMethod;            
    }

    public DCommand(Action executeMethod, Func<bool> canExecuteMethod)
        : this(executeMethod)
    {
        this.CanExecuteFunc = canExecuteMethod;
    }

    public bool CanExecute(object parameter=null)
    {
        if (CanExecuteFunc == null)
            return true;

        return CanExecuteFunc.Invoke();
    }

    public event EventHandler CanExecuteChanged;

    public void Execute(object parameter=null)
    {

        if (CanExecuteFunc == null || CanExecute(parameter))
        {
            Action.Invoke(parameter);                
        }

    }
}

After all, If you want to change your ViewModel property when textbox's text changes immediately you should bind by this way.

Text="{Binding BoundProperty,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"

Below is my code:

<StackPanel>
    <TextBox x:Name="TextBox1" Margin="5,0,5,0" Width="100">
         <i:Interaction.Triggers>
               <i:EventTrigger EventName="TextChanged">
                    <i:InvokeCommandAction Command="{Binding OnValidateExecute, Mode=OneWay}" CommandParameter="{Binding Text,ElementName=TextBox1}" />
               </i:EventTrigger>
         </i:Interaction.Triggers>
    </TextBox>

    <Button x:Name="btnSave" Width="120" Height="25" Content="click" IsEnabled="{Binding IsOk}">
         <i:Interaction.Triggers>
               <i:EventTrigger EventName="Click">
                    <i:InvokeCommandAction Command="{Binding OnSaveExecute}">
                    </i:InvokeCommandAction>
               </i:EventTrigger>
         </i:Interaction.Triggers>
    </Button>
</StackPanel>

Only add a command parameter.

public class myViewModel : INotifyPropertyChanged
{
    public bool IsOk { get; set; }
    public string Message { get; set; }
    public RelayCommand OnValidateExecute { get; set; }

    public myViewModel()
    {
        OnValidateExecute = new RelayCommand(p => 
            {
                Message = p as string;
                IsValid();
            }, p => true);
    }

    public bool IsValid()
    {
        bool valid = !string.IsNullOrEmpty(Message);
        IsOk = valid;
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs("IsOk"));
        }
        return valid;
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

It works well.

Hope this helps.

To complete Davut Gürbüz answer :

On Button you have a Command property , use this property is better than i:Interaction.Triggers (easyer to read).

Like Davut Gürbüz say RelayCommand have a CanExecuteFunc parameter . If you use this property, the state of your button changes automatically.

Xaml code :

<TextBox
    Text="{Binding BoundProperty,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
    ...other propeties here.... />
<Button
    x:Name="btnSave"
    Command="{Binding OnSaveExecute}"
    ...other propeties here.... />

C#

/// <summary>
/// Gets the OnSaveExecute.
/// </summary>
public RelayCommand OnSaveExecute
{
    get
    {
        return _onSaveExecute 
            ?? (_onSaveExecute = new RelayCommand(
                                    () =>
                                    {
                                              // Save action or call save method
                                    },
                                    () => IsOk));
    }
}
private RelayCommand _onSaveExecute;

/// <summary>
/// Sets and gets the IsOk property.
/// Changes to that property's value raise the PropertyChanged event.
/// </summary>
public bool IsOk
{
    get { return _isOk; }
    set
    {
        if (_isOk == value)
        {
            return;
        }
        _isOk = value;
        RaisePropertyChanged("IsOk");
        OnSaveExecute.RaiseCanExecuteChanged();
    }
}
private bool _isOk = false;

/// <summary>
/// Sets and gets the BoundProerty property.
/// Changes to that property's value raise the PropertyChanged event.
/// </summary>
public string BoundProerty
{
    get { return _boundProerty; }
    set
    {
        if (_boundProerty == value)
        {
            return;
        }
        _boundProerty = value;
        RaisePropertyChanged("BoundProerty");
        IsValid();
    }
}
private string _boundProerty = false;

public myViewModel()
{
}

public bool IsValid()
{
    // other codes
    IsOk = MethodName();
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM