Home > Software design >  How to get Text from TextBox to ViewModel while it is bound to other control?(MVVM)
How to get Text from TextBox to ViewModel while it is bound to other control?(MVVM)

Time:08-19

I have a simple app, that should add SelectedItem from ComboBox to ListBox. I have Model:Player

public class Player
{
    public int ID { get; set; }
    public string Name { get; set; }

    private bool _isSelected = false;
    public bool IsSelected
    {
        get { return _isSelected; }
        set { _isSelected = value; }
    }
}

And ObservableCollection property in my ViewModel (Players)

public class ViewModel
{
    public ObservableCollection<Player> Players { get; set; }
    public ObservableCollection<Player> PlayersInTournament { get; set; } = new ObservableCollection<Player>();
    public ICommand AddPlayerCommand { get; set; }
    public ViewModel()
    {            
        DataAccess access = new DataAccess();
        Players = new ObservableCollection<Player>(access.GetPlayers());//GetPlayers from DataBase
        AddPlayerCommand = new RelayCommand(AddPlayer, CanAddPlayer);
    }

    private void AddPlayer()
    {
        //Something like PlayersInTournamen.Add(SelectedPlayer);
    }

    private bool CanAddPlayer()
    {
        bool canAdd = false;
        foreach(Player player in Players)
        {
            if (player.IsSelected == true)
                canAdd = true;
        }

        return canAdd;
    }
}

Property(ItemSource) of my ComboBox is bound to the Players collection. When the application is Loaded my ComboBox is filled with objects from DataBase and when I select one of them it is displayed in my ReadOnly TextBox. I achieved this by binding the Text property to the ItemSelected.Name property of ComboBox. There is an Add button in the app that add selected player to the tournament(ListBox)(the app is about tournament). ListBox's ItemSource is PlayersInTournament collection(see in ViewModel).

XAML(DataContext of Window is set to ViewModel instance after InitializeComponents()):

<Window x:Class="ComboBoxDemoSQL.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:b="http://schemas.microsoft.com/xaml/behaviors"        
    xmlns:local="clr-namespace:ComboBoxDemoSQL"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800">

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition/>
    </Grid.ColumnDefinitions>

    <Grid.RowDefinitions>
        <RowDefinition/>   
        <RowDefinition/>   
    </Grid.RowDefinitions>
    <StackPanel>
        <StackPanel  HorizontalAlignment="Center"
                     Orientation="Horizontal" Margin="0 40 0 10">

            <TextBox x:Name="HoldPlayerTextBox"
                     Width="100"
                     Text="{Binding ElementName=PlayersComboBox, Path=SelectedItem.Name}"
                     IsReadOnly="True">
            </TextBox>

            <ComboBox Name="PlayersComboBox" 
                      VerticalAlignment="Top" 
                      Margin="10 0 0 0"
                      HorizontalAlignment="Center" Width="100"
                      ItemsSource="{Binding Players}"
                      DisplayMemberPath="Name"
                      Text="Select player"
                      IsEditable="True"
                      IsReadOnly="True"/>
        </StackPanel>

        <Button Content="Add" Margin="120 0 120 0"
                Command="{Binding AddPlayerCommand}"/>

        <ListBox Margin="10" ItemsSource="{Binding PlayersInTournament}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Name}"/>
                        <TextBlock Text="{Binding ID}"/>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </StackPanel>
</Grid>

Photo to understand better:

enter image description here

So basically there are 2 problems:

  1. I don't know how to add to the PlayersInTournament collection a player that is selected in ComboBox because I can't get the name of that Player from TexBox(because its' Text property is bound to another Property)
  2. I don't know how to disable Add Button(CanAddPlayer method) when there is no Player selected, I tried by adding IsSelected(see Player model) property, but for it to work I have to bind to any property in View that would change it, but I don't know which property can be used for this thing.

ICommand implementation:

public class RelayCommand : ICommand
{
    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested  = value; }
        remove { CommandManager.RequerySuggested -= value; }
    }
    private Action methodToExecute;
    private Func<bool> canExecuteEvaluator;
    public RelayCommand(Action methodToExecute, Func<bool> canExecuteEvaluator)
    {
        this.methodToExecute = methodToExecute;
        this.canExecuteEvaluator = canExecuteEvaluator;
    }
    public RelayCommand(Action methodToExecute)
        : this(methodToExecute, null)
    {
    }
    public bool CanExecute(object parameter)
    {
        if (this.canExecuteEvaluator == null)
        {
            return true;
        }
        else
        {
            bool result = this.canExecuteEvaluator.Invoke();
            return result;
        }
    }
    public void Execute(object parameter)
    {
        this.methodToExecute.Invoke();
    }
}

CodePudding user response:

You can use CommandParameter in xaml:

 <Button Content="Add" Margin="120 0 120 0"
         Command="{Binding AddPlayerCommand}"
         CommandParameter="{Binding Path=SelectedItem, Source=PlayersComboBox}"/>

in your ViewModel:

    private ICommand _addPlayerCommand;
    public ICommand AddPlayerCommand
    {
        get
        {
            if (_addPlayerCommand== null)
            {
                _addPlayerCommand= new RelayCommand(param => OnAddPlayerClicked(param));
            }
            return _addPlayerCommand;
        }
    }
    private void AddPlayer(object param)
    {
        Player selectedPlayer = (player)param;
        PlayersInTournamen.Add(SelectedPlayer);
    }

I hope this helps.

CodePudding user response:

May I suggest the following.

You can override the ToString() method of your Player class to ease display in your ComboBox e.g.:

public class Player
{
    public string Name { get; set; }
    public override string ToString()
    {
        return Name;
    }
}

By default ComboBox binding will call the ToString() method of whatever property it is bound to.

If you bind ComboBox.SelectedItem to a new Player property in the ViewModel, you can clear the selected player text in the ComboBox from code in the ViewModel.

If you add a CommandParameter to your Button binding, you can pass the selected player instance to the command, but this isn't strictly needed once you have a bound property in your ViewModel.

Thus your XAML becomes something like this:

<ComboBox x:Name="ComboBox" 
          HorizontalAlignment="Left" 
          Margin="0,0,0,0" 
          VerticalAlignment="Top" 
          Width="100"
          Text="Select player"
          SelectedItem="{Binding SelectedPlayer}"
          ItemsSource="{Binding Players}"/>
<Button x:Name="ButtonAddPlayer" 
        Content="Add" 
        Command="{Binding AddPlayerCommand}" 
        CommandParameter="{Binding SelectedPlayer}" 
        HorizontalAlignment="Left" 
        Margin="62,176,0,0" 
        VerticalAlignment="Top" 
        Width="75"/>

And your ViewModel contains:

public ObservableCollection<Player> PlayersInTournament { get; set; }
public ObservableCollection<Player> Players { get; set; }
private Player _selectedPlayer;

public Player SelectedPlayer
{
    get => _selectedPlayer;
    set => SetField(ref _selectedPlayer, value);
}

public ICommand AddPlayerCommand { get; set; }

private bool CanAddPlayer(object obj)
{
    return SelectedPlayer != null;
}
private void AddPlayer(object param)
{

    if (param is Player player)
    {
        PlayersInTournament.Add(player);
        Players.Remove(player);
        SelectedPlayer = null;
    };
}

Note that in the above code, as a player is added to the tournament list it is removed from the available players list preventing reselection of the same player. Setting the SelectedPlayer property to null not only clears the ComboBox.SelectedItem display but also disables the Add button.

Also if you are likely to have several properties that you implement a helper function to handle your INotifyPropertyChanged events.

protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
    if (EqualityComparer<T>.Default.Equals(field, value)) return false;
    field = value;
    OnPropertyChanged(propertyName);
    return true;
}
  • Related