0

I've three Classes in my project Master, Person and Command. Master has two properties, a constructor and overridden the ToString:

class Master {
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public Master(string FirstName, string LastName) {
        this.FirstName = FirstName;
        this.LastName = LastName;
    }

    public override string ToString() {
        return FirstName + " " + LastName;
    }
}

Command is an implementation of ICommand

class Command : ICommand {
    Func<object, bool> CanDo { get; set; }
    Action<object> Do { get; set; }
    public event EventHandler CanExecuteChanged;

    public Command(Func<object, bool> CanDo, Action<object> Do) {
        this.CanDo = CanDo;
        this.Do = Do;
        CommandManager.RequerySuggested += (o, e) => Evaluate();
    }

    public bool CanExecute(object parameter) => CanDo(parameter);
    public void Execute(object parameter) => Do(parameter);
    public void Evaluate() => CanExecuteChanged?.Invoke(null, EventArgs.Empty);
}

and Person has two properties, implemented INotifyPropertyChanged, is an ObservableCollection<Master> and using Command:

class Person : ObservableCollection<Master>, INotifyPropertyChanged {

    string firstName, lastName;

    public string FirstName {
        get => firstName;
        set { firstName = value; OnPropertyChanged(); }
    }

    public string LastName {
        get => lastName;
        set { lastName = value; OnPropertyChanged(); }
    }

    public Command AddToList { get; set; }
    public new event PropertyChangedEventHandler PropertyChanged;

    public Person() {
        AddToList = new Command(CanDo, Do);
    }

    void OnPropertyChanged([CallerMemberName] string name = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));        
    bool CanDo(object para) => !string.IsNullOrEmpty(firstName) && !string.IsNullOrEmpty(lastName);
    void Do(object para) {
        Add(new Master(firstName, firstName));
        FirstName = LastName = null;
    }
}

On xaml I've these:

<Window ...>
    <Window.Resources>
        <local:Person x:Key="Person"/>
    </Window.Resources>

    <Grid DataContext="{StaticResource Person}">
        <StackPanel>
            <TextBox Text="{Binding FirstName}"/>
            <TextBox Text="{Binding LastName}"/>
            <Button Content="Click" Command="{Binding AddToList}"/>
            <ListView ItemsSource="{Binding}"/>
        </StackPanel>
    </Grid>
</Window>

I've to click on the first TextBox, bound to FirstName, after launching the app to type something there, by pressing Tab I can type in the second TextBox and if I then hit Tab again, it instead of focusing the Button goes back to first TextBox so I've to hit Tab twice to get to the Button and by hitting Enter or Space I can add the item in the ListView.

At this point I'm not sure what's focused, I've to hit Tab once more to get to the first TextBox. After typing some more text in first as well as second TextBoxes if I hit Tab, it instead of focusing Button or first TextBox selects the ListView so I've to hit Tab thrice to get to the Button!

I want to give first TextBox focus when the app launches and after hitting Tab on second TextBox I want it to go to the Button and exclude ListView from focus. I've tried setting Focusable="False", KeyboardNavigation.IsTabStop="False", IsTabStop="False" in ListView but those don't work! I also have tried settingTabIndex on TextBoxes and Button like this:

<TextBox Text="{Binding FirstName}" TabIndex="1"/>
<TextBox Text="{Binding LastName}" TabIndex="2"/>
<Button Content="Click" Command="{Binding AddToList}" TabIndex="3"/>

These don't work either!

2 Answers2

0

The problem you're having is that at the point you tab away from the second text box it needs to decide where to place the focus. However, at that point in time, the command is still disabled because it cannot be executed because the value from the text box has not arrived in the view model yet. This value arrives after the focus has been moved.

One way to fix this would be to change the binding of the second textbox so that the ViewModel is updated on every change to the value. Add the following clause to that binding:

UpdateSourceTrigger=PropertyChanged

More details here... https://learn.microsoft.com/en-us/dotnet/framework/wpf/data/how-to-control-when-the-textbox-text-updates-the-source

Now whenever you type a character the command will reconsider whether it can be executed.

Richardissimo
  • 5,596
  • 2
  • 18
  • 36
  • great! when the app launches it still doesn't focus the first `TextBox` and when I hit `space` or `enter` on `Button` it doesn't go back to first `TextBox`, I've to hit tab to take it back to the first box. –  Oct 13 '19 at 21:44
  • 2
    The initial focus issue is a bizarre 'feature' of WPF see https://stackoverflow.com/questions/817610/wpf-and-initial-focus for a fix for that. – Richardissimo Oct 13 '19 at 21:46
  • 1
    And to see where the focus is going after the button, try using a WPF developer support app like Snoop or WpfInspector. If you keep tabbing, it will come back around. You just need to find where it went and make sure that can't receive the focus. – Richardissimo Oct 13 '19 at 21:48
0

The short answer:

Set UpdateSourceTrigger=PropertyChanged on your binding:

<TextBox Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Text="{Binding LastName, UpdateSourceTrigger=PropertyChanged}"/>

I suspect what is happening is that WPF is evaluating the next tab position before the command CanExecute is evaluated, so when it is working out where to tab to next the button is still disabled, hence the only place it has to go is back to the first text box.

UpdateSourceTrigger tells WPF to evaluate the bindings on each keypress instead of when the focus changes, which means the button is correctly enabled by the time it needs to work the tab stops out.

MarcE
  • 3,586
  • 1
  • 23
  • 27
  • For the first time I've to click on the first box and after hitting space on button I've to tab again to go to first box. –  Oct 13 '19 at 21:45