Problème de binding après la sélection d’un élément dans une listbox en Silverlight

En Silverlight, le binding de la propriété Text d’une TextBox se fait lorsque celle-ci perd le focus.

Le problème exposé dans cet article apparaît lorsque qu’une TextBox (avec sa propriété Text bindée dans le ViewModel de la page associée) précède une ListBox (avec un binding sur la propriété SelectedItem). Si le setter du SelectedItem fait intervenir la propriété bindée à la propriété Text de la TextBox alors celui-ci ne prendra pas en compte le contenu de la TextBox.

Le code sera plus parlant (Cet article utilise MVVMLight) :

MainPage.xaml

<StackPanel x:Name="LayoutRoot">
  <TextBox Text="{Binding MyText, Mode=TwoWay}" />
  <TextBox Text="Texte" />
  <ListBox ItemsSource="{Binding MyList}" 
           SelectedItem="{Binding MySelected, Mode=TwoWay}">
  </ListBox>
  <TextBlock Text="{Binding MyResult, Mode=TwoWay}" />
</StackPanel>

MainViewModel.cs

public class MainViewModel : ViewModelBase
{
    public MainViewModel()
    {
        if (IsInDesignMode)
        {
            // Code runs in Blend --> create design time data.
        }
        else
        {
            MyList = new List<string>() { "test 1", "test 2", "test 3" };
        }
    }

    private string _MyText;
    public string MyText
    {
        get { return _MyText; }
        set { _MyText = value; RaisePropertyChanged("MyText"); }
    }
    
    private List<string> _MyList;
    public List<string> MyList
    {
        get { return _MyList; }
        set { _MyList = value; RaisePropertyChanged("MyList"); }
    }
    
    private string _MySelected;
    public string MySelected
    {
        get { return _MySelected; }
        set
        {
            _MySelected = value;
            RaisePropertyChanged("MySelected");
            MyResult = string.Format("{0} - {1}", MySelected, MyText);
        }
    }
    
    private string _MyResult;
    public string MyResult
    {
        get { return _MyResult; }
        set { _MyResult = value; RaisePropertyChanged("MyResult"); }
    }
}

Voici les 2 scénarii illustrant ce problème :

  • Remplissez la 1re TextBox avec du texte, cliquez ensuite sur un élément de la ListBox (sans cliquez à un autre endroit avant). Le binding ne se fait pas correctement.

Le résultat est :

Scenario 1

  • Remplissez la 1re TextBox, cliquez sur la TextBox contenant le mot “Texte” et choisissez ensuite un élément de la ListBox. Le binding est correct.

Le résultat est :

Scenario 2

Explications

Dans le 1er cas, le binding de la ListBox se fait avant le binding de la TextBox. Le setter du SelectedItem de la ListBox utilise la propriété MyText, mais celle-ci n’a pas encore été bindée, le contenu de MyText est donc vide.

Dans le 2ème cas, on sélectionne la 2ème TextBox, ce qui fait perdre le focus à la 1re et binde le contenu de la TextBox à la propriété MyText. Au click sur un des éléments de la ListBox, MyText est déjà rempli, le résultat est donc correct.

Comment y remédier ?

  • La 1re approche consiste à déclencher un trigger sur la 1re TextBox lors de l'événement KeyUp. A chaque fois que la TextBox a le focus et qu’une touche du clavier est pressée, la propriété MyText est mise à jour avec le contenu de la TextBox.

MainPage.xaml

<StackPanel x:Name="LayoutRoot">
  <TextBox Text="{Binding MyText, Mode=TwoWay}" x:Name="txtBoxToBind">
    <i:Interaction.Triggers>
      <i:EventTrigger EventName="KeyUp">
        <cmd:EventToCommand Command="{Binding BindCommand}"
                            CommandParameter="{Binding ElementName=txtBoxToBind, 
                            Path=Text}" />
      </i:EventTrigger>
    </i:Interaction.Triggers>
  </TextBox>
  <ListBox ItemsSource="{Binding MyList}" 
           SelectedItem="{Binding MySelected, Mode=TwoWay}">
  </ListBox>
  <TextBlock Text="{Binding MyResult, Mode=TwoWay}" />
</StackPanel>

MainViewModel.cs

public class MainViewModel : ViewModelBase
{
    public MainViewModel()
    {
        if (IsInDesignMode)
        {
            // Code runs in Blend --> create design time data.
        }
        else
        {
            MyList = new List<string>() { "test 1", "test 2", "test 3" };
            BindCommand = new RelayCommand<string>(ExecuteBindCommand);
        }
    }

    private string _MyText;
    public string MyText
    {
        get { return _MyText; }
        set { _MyText = value; RaisePropertyChanged("MyText"); }
    }
    
    private List<string> _MyList;
    public List<string> MyList
    {
        get { return _MyList; }
        set { _MyList = value; RaisePropertyChanged("MyList"); }
    }
    
    private string _MySelected;
    public string MySelected
    {
        get { return _MySelected; }
        set
        {
            _MySelected = value;
            RaisePropertyChanged("MySelected");
            MyResult = string.Format("{0} - {1}", MySelected, MyText);
        }
    }
    
    private string _MyResult;
    public string MyResult
    {
        get { return _MyResult; }
        set { _MyResult = value; RaisePropertyChanged("MyResult"); }
    }
    
    public RelayCommand<string> BindCommand { get; set; }
    
    private void ExecuteBindCommand(string text)
    {
        MyText = text;
    }
}
  • La 2ème approche consiste à utiliser le code-behind du fichier View (MainPage.xaml). Le rôle du MVVM est de réduire la liaison entre la View et le ViewModel en n’utilisant pas (ou peu) le code-behind de la View. Dans le cas présent, le code-behind nous sert uniquement à rafraichir le binding lors de l'événement KeyUp sur la TextBox (et ne renforce donc pas la liaison avec le ViewModel).

MainPage.xaml

<StackPanel x:Name="LayoutRoot">
  <TextBox Text="{Binding MyText, Mode=TwoWay}" x:Name="txtBoxToBind" 
           KeyUp="txtBoxToBind_KeyUp" />
  <ListBox ItemsSource="{Binding MyList}" 
           SelectedItem="{Binding MySelected, Mode=TwoWay}">
  </ListBox>
  <TextBlock Text="{Binding MyResult, Mode=TwoWay}" />
</StackPanel>

MainPage.xaml.cs

private void txtBoxToBind_KeyUp(object sender, System.Windows.Input.KeyEventArgs e)
{
    BindingExpression binding = 
        (sender as TextBox).GetBindingExpression(TextBox.TextProperty);
    binding.UpdateSource();
}

Voir également