简体   繁体   中英

Disable button click event without disabling command?

I'm creating application in WPF using MVVM where I display a page with save file name and path textboxes. These textboxes are heavily validated in real time (path too short, too long, invalid characters, etc), and the "save" button will only become active if both file and path are valid. However, I'm leaving some final checks like does the directory exist, file already exist, etc until the user hits the save button.

Problem is that I cheat a bit and use the button click for the Navigation Service and the command to do any last minute saving or cleanup in the ViewModel. In this case, the command will process but when it finishes, regardless of the validation result, the click fires immediately after. So, somehow I need to enable or disable just the click event until we know the final validation result.

View:

<TextBlock>
    Backup Name:
</TextBlock>

<TextBlock Text="{Binding Path=(Validation.Errors)/ErrorContent, 
                   ElementName=BackupFileNameTextBox}"/>

<TextBox x:Name="BackupFileNameTextBox"
         Text="{Binding BackupFileName, Mode=TwoWay, 
                UpdateSourceTrigger=PropertyChanged, 
                ValidatesOnNotifyDataErrors=True}"/>

... File Path Code same as above...

<Button Click="Pg9SaveBtnClick"
        Command="{Binding SaveBtnCommand}">
    Save
</Button>

Code Behind

public Pg9BackupIntro()
{
   InitializeComponent();
}

private void Pg9SaveBtnClick(object sender, RoutedEventArgs e)
{
   // What I want to do
   if(binding.ViewModel.SaveClickEnabled) //<- Something like this
   {
       if (NavigationService.CanGoForward)
           NavigationService.GoForward();
       else
       {
           Pg10Backup page = new Pg10Backup();
           NavigationService.Navigate(page);
       }
   }
}

private void Pg9BackBtnClick(object sender, RoutedEventArgs e)
{
    NavigationService.GoBack();
}

ViewModel:

public RelayCommand SaveBtnCommand { get; private set; }

public Pg9BackupIntroViewModel()
{
   SetupBackupSave();
   SaveBtnCommand = new RelayCommand(SetBackupFileData, CanSetBackupFileData);    
}

private string _backupFileName;
public string BackupFileName
{
    get { return _backupFileName; }
    set
    {
        _backupFileName = value;
        validateFileName();
        RaisePropertyChanged();
     }
}

... BackupFilePath prop same as above...

private bool _saveClickEnabled = false;
public bool SaveClickEnabled
{
    get { return _saveClickEnabled; }
    set
    {
        _saveClickEnabled = value;
        RaisePropertyChanged();
    }
}

private bool CanSetBackupFileData(object obj)
{
    if(HasErrors)
        return false;

    return true;
}

private void SetBackupFileData(object obj)
{
    finalValidation();

    if(!HasErrors)
       SaveClickEnabled = true;
    else 
       SaveClickEnabled = false;
}

This simplest thing (shown above) I can think of is to create a property in the VM called "SaveClickEnable" and in the code behind bind to this property. Then we can simply check the property in the click event handler and call the Navigation Service if things are OK. Unfortunately, all of the tutorials I've looked at only cover binding through UI elements like Textboxes, not bind to a code behind variable. Here's the approach to bind I was trying:

public Pg9BackupIntro()
{
   InitializeComponent();
   Binding binding = new Binding();
   binding.Source = ViewModel or Datacontext;  //<- don't understand syntax here
   binding.Path = ViewModel.SaveClickEnabled;
}

Another thought was to maybe use 2 buttons; 1 for the command and 1 for the click . The click button will never be visible but somehow trigger the click based on a property set in the VM.

Are either of these possible? I've tried to do them both but my noobness got in the way. I've also tried binding to IsHitTestVisible but this also disables the command .

<Button IsHitTestVisible="{Binding SaveClickEnabled}" //<- Disables command too
        Click="Pg9SaveBtnClick"
        Command="{Binding SaveBtnCommand}">
    Save
</Button>

Is there a simple way to do this?

I solved this by creating a hidden checkbox and binding it's IsChecked property to SaveClickEnabled . Then I simply check the value of the checkbox in the click method and it bypasses the navigation code if there's a error.

View:

<CheckBox x:Name="ClickEnabledCheckbox" 
                  Visibility="Collapsed" 
                  IsChecked="{Binding SaveClickEnabled}"/> 

Code-Behind:

private void Pg9BackupBtnClick(object sender, RoutedEventArgs e)
{
    if(ClickEnabledCheckbox.IsChecked == true)
    {
        ...Navigate to next View
    }
} 

ViewModel:

private bool _saveClickEnabled = false;
public bool saveClickEnabled
{
    get { return _backupClickEnabled; }
    set
    {
        if (HasErrors)
            _backupClickEnabled = false;

         _backupClickEnabled = true;
         RaisePropertyChanged();
     }
}

The beauty of it is that I didn't want to be barking at the user the entire time they might be on their way to typing in a valid file path or one that we can create for them. So this only checks for those kinds of errors on click/command, and the second the user tries to correct his/her mistake, the error goes away until next press.

My only wish was it was a bit cleaner, seems like there should be a ways to eliminate the checkbox.

If I understand you correctly, then you need to call a certain method (in your code it is Pg9SaveBtnClick) after the command is executed in the ViewModel (that is, after the SetBackupFileData method).
Conceptually, you are faced with the problem: "How can the ViewModel call a method that it does not know about?".
If you think about it a little, then the answer to it begs itself: "It is necessary to pass information about this method to the ViewModel." Method information is passed through a delegate.
Typically, this transfer of information occurs at the time the ViewModel is instantiated and is referred to as "dependency injection".

Here's an example of such an implementation:

public class ViewModel
{
    private readonly Action executeAfterSaving;
    public ViewModel(Action executeAfterSaving)
       => this.executeAfterSaving = executeAfterSaving;

    private void SetBackupFileData(object obj)
    {
        finalValidation();

        if(!HasErrors)
            executeAfterSaving?.Invoke();
    }

     // Continuation of the code
}

Code Behind the Window:

     {
        // Some code
 
        DataContext = new ViewModel(GoForwardAfterSaving);
     }

     private void GoForwardAfterSaving()
     {
         if (NavigationService.CanGoForward)
             NavigationService.GoForward();
         else
         {
             Pg10Backup page = new Pg10Backup();
             NavigationService.Navigate(page);
         }
     }

The code is shown in general terms. For a more specific example, we need more details about your implementation.

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