-4

I have a ListBox whose ListBoxItems are a control derived from UserControl. Each of these controls contain a UserControl derived from Button and some TextBoxes. I want to bind these to elements in my model by using an ItemTemplate in the ListBox, but I'm having trouble assigning values to the button from the ItemTemplate.

The ListViewer contains the ListBox and its DataTemplate:

<UserControl x:Class="TestListBoxBinding.ListViewer"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:model="clr-namespace:Model"
             xmlns:vm="clr-namespace:ViewModel"
             xmlns:local="clr-namespace:TestListBoxBinding"
             mc:Ignorable="d">
    <UserControl.DataContext>
        <vm:ListViewerViewModel/>
    </UserControl.DataContext>
    <UserControl.Resources>
        <DataTemplate DataType="{x:Type model:Person}">
            <Grid Name="theListBoxItemGrid">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="Auto"/>
                </Grid.ColumnDefinitions>
                <local:PersonButton Grid.Column="0" x:Name="thePersonButton"
                                    FirstName="Ken"
                                    LastName="Jones"/>
                <TextBlock Grid.Column="1" Name="theFirstName" Text="{Binding FirstName}" Margin="5,0,5,0"/>
                <TextBlock Grid.Column="2" Name="theLastName" Text="{Binding LastName}" Margin="0,0,5,0"/>
            </Grid>
        </DataTemplate>
    </UserControl.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <TextBlock Grid.Row="0" Name="theHeader" Text="These are my people"/>
        <ListBox Grid.Row="1" Name="theListBox"
                 ItemsSource="{Binding MyPeople}"/>
        <StackPanel Grid.Row="2" Name="theFooterPanel" Orientation="Horizontal">
            <TextBlock Name="theFooterLabel" Text="Count = "/>
            <TextBlock Name="theFooterCount" Text="{Binding MyPeople.Count}"/>
        </StackPanel>
    </Grid>
</UserControl>

And its code behind:

using Model;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;

namespace TestListBoxBinding
{
    /// <summary>
    /// Interaction logic for ListViewer.xaml
    /// </summary>
    public partial class ListViewer : UserControl
    {
        public ListViewer()
        {
            InitializeComponent();
        }
    }
}

namespace ViewModel
{
    public class ListViewerViewModel
    {
        // Properties

        public People MyPeople { get; private set; }

        // Constructors

        public ListViewerViewModel()
        {
            MyPeople = (People)Application.Current.Resources["theOneAndOnlyPeople"];
            MyPeople.Add(new Person("Able", "Baker"));
            MyPeople.Add(new Person("Carla", "Douglas"));
            MyPeople.Add(new Person("Ed", "Fiducia"));
        }
    }
}

namespace Model
{
    public class Person
    {
        // Properties

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

        // Constructors

        public Person()
        {
            FirstName = "John";
            LastName = "Doe";
        }

        public Person(string firstName, string lastName)
        {
            this.FirstName = firstName;
            this.LastName = lastName;
        }
    }

    public class People : ObservableCollection<Person>
    {
    }
}

The PersonButton has FirstName and LastName DependencyProperties defined in code behind that I will eventually use to set the button's contents for display. Here's its xaml:

<UserControl x:Class="TestListBoxBinding.PersonButton"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:TestListBoxBinding"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <Button Name="theButton" Padding="5,0,5,0"/>
    </Grid>
</UserControl>

And its code behind:

using System.Windows;
using System.Windows.Controls;

namespace TestListBoxBinding
{
    /// <summary>
    /// Interaction logic for PersonButton.xaml
    /// </summary>
    public partial class PersonButton : UserControl
    {
        // Properties

        public static readonly DependencyProperty FirstNameProperty = DependencyProperty.Register("FirstName", typeof(string), typeof(PersonButton));
        public string FirstName { get { return (string)GetValue(FirstNameProperty); } set { SetValue(FirstNameProperty, value); } }

        public static readonly DependencyProperty LastNameProperty = DependencyProperty.Register("LastName", typeof(string), typeof(PersonButton));
        public string LastName { get { return (string)GetValue(LastNameProperty); } set { SetValue(LastNameProperty, value); } }

        // Constructors
        public PersonButton()
        {
            InitializeComponent();
            FirstName = "Ira";
            LastName = "Jacobs";
            theButton.Content = FirstName + " " + LastName;
        }
    }
}

As you can see from the output:

enter image description here

The attempts to set the FirstName and LastName properties of the PersonButton from the DataTemplate for the ListBox fail. I set breakpoints on the property sets and they only hit when the controls were constructed, but never from the DataTemplate. I used the Live Visual Tree to confirm that the properties were, in fact, still set as they were initialized.

I should mention, too, that initially, I got an Intellisense warning that the properties were not accessible from the DataTemplate, but after I deleted the bin and obj folders, and cleaned and rebuilt the solution, that went away.

Update

I believe this is not the same as the suggested duplicate in the comments.

As to PersonButton not making any sense, this whole collection of code is a greatly simplified representation of a piece of a much more complex application that I created simply to show the problem I'm having in a more easily read form. I have tested this code and it duplicates the problem I'm having exactly. The actual control that PersonButton represents is much more complex and the use of properties defined in the code behind is entirely appropriate for that control. The only reason PersonButton is doing what it is doing is to make it easy to tell if those properties got set by the assignments.

Now, to address my question here, it is, "Why are PersonButton.FirstName and PersonButton.LastName not getting changed by the their assignment statements for thePersonButton in the DataTemplate for theListBox in my ListViewer control?"

As I mentioned before, I have run several tests, and they all verify that those values in the actual control are not getting set by the assignments. Since seeing Clemens' reply, I have even implemented the PropertyChangedCallback functions, and they, too, confirm that the PersonButton properties are not getting set by the ListBox assignments.

Of specific interest to me is that these assignments will become bindings themselves that allow me to capture changes to each individual item in the list in my Model through the ItemsSource mechanism. In my original application, those bindings work, but my problem is in actually assigning them to the PersonButton control.

Clemens
  • 123,504
  • 12
  • 155
  • 268
  • 1
    Besides that you did not register PropertyChangedCallbacks for the dependency properties of your PersonButton, the whole PersonButton UserControl makes no sense. You can easily replace it with an ordinary Button where you bind the Content by a MultiBinding with a StringFormat. Done. – Clemens Jun 29 '19 at 06:29
  • I believe you misunderstood my question. I have edited my question so as (I hope) to make that question more clear. You did, though, provide information that will help me to do other things I planned to, so thanks! – harleygnuya Jun 29 '19 at 13:47
  • Possible duplicate of [XAML binding doesn't seem to set if the property is initialized in the constructor](https://stackoverflow.com/questions/1779022/xaml-binding-doesnt-seem-to-set-if-the-property-is-initialized-in-the-construct) – Peter Duniho Jun 30 '19 at 00:21
  • Your question is a duplicate, just not of the one @Clemens had proposed. Your posted answer also is not correct. While it's true that you should not be setting the property in the construction, this isn't a "timing" thing as you claim. You can verify that by adding `Thread.Sleep()` in the constructor to ensure that the later update of the context does not happen quickly. Rather, what happens is that you've overridden the template binding with a local assignment (i.e. call to `SetValue()`, which disables the original binding in the template. Yes, the fix is "don't do that". – Peter Duniho Jun 30 '19 at 00:23
  • Thanks, Peter. I added some tracing and that, together with the PresentationTraceSources confirm what you said. It shows that a binding expression is being created, but it does not appear to actually get applied to the PersonButton. Interesting. The other post you referenced mentioned the DependencyProperty's value precedence, as I did, but I assumed that that came into play only if there was a "collision" when setting the value of the property, hence my comment about the timing... – harleygnuya Jun 30 '19 at 06:38
  • When you say that making a local assignment disables the original binding in the template, do you mean permanently? If that's the case, then it would seem that one should never make a local assignment to a property that he intends to bind. Is that correct? – harleygnuya Jun 30 '19 at 06:40
  • _"When you say that making a local assignment disables the original binding in the template, do you mean permanently?"_ -- yes, permanently. _"one should never make a local assignment to a property that he intends to bind. Is that correct?"_ -- yes, that is correct. The most common way people get themselves into this situation is in the XAML (you should avoid setting properties and bindings in code-behind in the first place). ... – Peter Duniho Jul 03 '19 at 22:43
  • ... They will set one value in the object's XAML declaration, and then have a conditional value set in a style's setter. The style's setter never winds up working, because the local assignment in the declaration takes precedence always. That's just a specific example of this more general concept. – Peter Duniho Jul 03 '19 at 22:44

1 Answers1

-2

I found the problem! It has to do with dependency property value precedence. When the same dependency property is set more than once in quick succession (close enough that they may interfere with each other) the system applies a precedence to their handling. In essence locally set values are handled with higher priority than remotely set values.

In this case, when the ListViewViewModel initializes the People collection in the Model, the property changed handler hits twice in rapid succession (often less than 1 ms): once when the Person is constructed and once when the Person is added to the collection (and, hence, the ListBox).

    public ListViewerViewModel()
    {
        MyPeople = (People)Application.Current.Resources["theOneAndOnlyPeople"];
        MyPeople.Add(new Person("Able", "Baker"));
        MyPeople.Add(new Person("Carla", "Douglas"));
        MyPeople.Add(new Person("Ed", "Fiducia"));
    }

Thus, they conflict with each other and the system gives precedence to the value set by the PersonButton's constructor, because it is local.

I commented out the assignment of the default values in the PersonButton constructor and, voila, the assignment from the ListBox worked.

    public Person()
    {
        FirstName = "John";
        LastName = "Doe";
    }

This was a PITA to chase down, but now that I have the answer, it was also kind of fun.