Search code examples
c#wpfdata-bindinglistboxitemtemplate

WPF: Trouble getting user changes in a ListBox template back to the Collection it's bound to


I have a standard ListBox with a template:

    <ListBox Name="statBox" ItemsSource="{Binding Path=p_statList}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <WrapPanel>
                    <TextBlock Text="{Binding Path=statName}" />
                    <TextBox Text="{Binding Path=statValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextChanged="TextChangedHandler" />
                </WrapPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>

public partial class MainWindow : Window
{
    Controller controller = new Controller();

    public MainWindow()
    {
        InitializeComponent();
        DataContext = controller;
    }

    private void TextChangedHandler(object sender, TextChangedEventArgs args)
    {
    }
}

public struct Stat
{
    public string statName { get; set; }
    public int statValue { get; set; }

    public Stat(string stat, int value)
    {
        statName = stat;
        statValue = value;
    }
}

internal class Controller : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    private ObservableCollection<Stat> statList = new ObservableCollection<Stat>();
    public ObservableCollection<Stat> p_statList
    {
        get { return statList; }
        set {
            statList = value;
        }
    }

    public Controller()
    {
        p_statList.Add(new Stat("Attack", 2));
        p_statList.Add(new Stat("Defense", 3));
        p_statList.Add(new Stat("Luck", 4));
    }

    public void RaisePropertyChanged(string property)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(property));
        }
    }
}

The issue I'm having is that there's no obvious way to distinguish value changes by the user among the text boxes.

  • Two-way binding doesn't seem to update the values in the ObservableCollection the ListBox is bound to. I've read that controls don't have the ability to set values inside collections. I'm not sure how true that is, but it's not working here.
  • I thought I could use a TextChangedEventHandler to detect a change in any box and send the new value to the corresponding Stat in the collection (p_statList[x]). Since boxes from a template aren't named, however, I can't tell which box the event came from. Hence, I can't tell which property to send it to. I tried setting the element name by binding to a member of the custom type in the collection like Name="{Binding Path=controlName}", but C# did not like that at all.
  • I was hoping there was some custom field I could add, like Tags="{Binding Path=myName}", but I'm not finding anything in my research.

An overly-complicated solution would be to traverse the UI recursively and look for the nth occurence of a TextBox, but that's very hacky and isn't sustainable if the UI changes.

The obvious solution is to just abandon the ListBox and the ObservableCollection and hard-code separate values to individual text boxes I can easily bind to, but that feels very un-savvy.

EDIT:

I've updated the above code with a more complete example that includes the C#. Some naming has changed, but the functionality is identical.

A breakpoint inside p_statList.set is never hit. I also tried breaking in TextChangedHandler to check controller.p_statList[0] in the immediate window, and the value never changes from 2.


Solution

  • Keep in mind binding here will change the properties of an instance not the instance itself.

    Make sure your custom type Stat is a mutable reference type (a class with writeable properties) and also consider implementing INotifyPropertyChanged which will help you notify the UI back in case needed.

    public class Stat : INotifyPropertyChanged
    { 
        public event PropertyChangedEventHandler? PropertyChanged;
    
        public int _statValue;
        public int statValue
        {
            get { return _statValue; }
            set
            {
                _statValue = value;
                PropertyChanged?.Invoke(this,
                    new PropertyChangedEventArgs(nameof(statValue)));
            }
        }
    
        ...
    } 
    

    As for handling TextChanged yourself, DataContext of the sender TextBox is the (playerStat) instance you're looking for. Just cast sender to TextBox and then cast DataContext to your custom type then use it, No need to traverse the tree to figure the index.