-2

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.

Nightmare Games
  • 2,205
  • 6
  • 28
  • 46
  • Could you show the code of playerStats and the class of its items? _controls don't have the ability to set values inside collections_ is simply wrong. – emoacht Jul 01 '23 at 07:31
  • Please be more specific about your problem, for example what is *"the correct property"*. You usually bind the TextBox.Text to the *"correct property"*. In your case editing the TetxtBox will modify the statValue property of the current item. You get the currently edited item by referencing the ListBox.SelectedItem property. It's not clear what you want. –  Jul 01 '23 at 11:55
  • @emoacht Thanks for that insight. I'd love to be wrong here. Question updated. – Nightmare Games Jul 01 '23 at 22:43
  • @BionicCode. I hope my edits clarified that. I can easily access the user-changed value in the TextChangedHandler, but it's not clear which trait in the collection I should put that value into. – Nightmare Games Jul 02 '23 at 01:04
  • 1
    @NightmareGames I guess you misunderstand what will be updated by the binding in the case of collection. When TextBox's Text is changed, the statValue property of an instance of Stat which is an item of p_statList will be updated. In other words, p_statList will not be updated but its item is. You can capture it at the setter of statValue property. – emoacht Jul 02 '23 at 04:32
  • 2
    Consequently, `Stat` must be a reference type, not a value type, i.e. a `class`, not a `struct`, and needs to implement `INotifyPropertyChanged`. – Clemens Jul 02 '23 at 06:48
  • The `statList` collection should be a readonly property, since it is never re-set and does not notify about changes: `public ObservableCollection statList { get; } = new ObservableCollection();` (use the name `statList` in the ItemsSource Binding). The INotifyPropertyChanged implementation of the Controller class seems useless anyway. – Clemens Jul 02 '23 at 06:53
  • 1
    Also consider to adhere to widely accepted naming conventions. Property names in C# are usually written like class names with PascalCasing, i.e. an uppercase first letter. – Clemens Jul 02 '23 at 06:55

1 Answers1

1

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.

Clemens
  • 123,504
  • 12
  • 155
  • 268
yassinMi
  • 707
  • 2
  • 9
  • Stat.statName and Stat.value do have public getters and setters already, if you take a look. INotifyPropertyChanged is implemented, but I'm not sure how that helps me in this specific example, unless your idea is to raise it from the TextChangedHandler. – Nightmare Games Jul 02 '23 at 00:58
  • @Clemens Thanks - those edits cleared up a lot of ambiguity for me. – Nightmare Games Jul 03 '23 at 21:44