Boris
Boris

Reputation: 10266

WPF Data binding: Changing items in the binded collection does not update binding source?

I have two classes:

public class Person
{
    public string FirstName
    {
        get { return firstName; }
        set { firstName = value; }
    }
    public string LastName
    {
        get { return lastName; }
        set { lastName = value; }
    }
    public ObservableCollection<AccountDetail> Details
    {
        get { return details; }
        set { details = value; }
    }
    public ObservableCollection<AccountDetail> Phones
    {
        get
        {
            ObservableCollection<AccountDetail> phones;
            phones = new ObservableCollection<AccountDetail>();
            foreach (AccountDetail detail in Details)
            {
                if (detail.Type == DetailType.Phone)
                {
                    phones.Add(detail);
                }
            }
            return phones;
        }
        set
        {
            ObservableCollection<AccountDetail> phones;
            phones = value;
            foreach (AccountDetail detail in Details)
            {
                if (detail.Type == DetailType.Phone)
                {
                    Details.Remove(detail);
                }
            }
            foreach (AccountDetail detail in phones)
            {
                if (!string.IsNullOrEmpty(detail.Value))
                {
                    Details.Add(detail); 
                }
            }
        }
    }

    private string firstName;
    private string lastName;
    private ObservableCollection<AccountDetail> details;
}

and

public class AccountDetail
{
    public DetailType Type
    {
        get { return type; }
        set { type = value; }
    }
    public string Value
    {
        get { return this.value; }
        set { this.value = value; }
    }

    private DetailType type;
    private string value;
}

In my XAML file I have a ListBox named PhonesListBox which is data bound to the phones list (a property of the Person object):

<Window.Resources>

    <!-- Window controller -->
    <contollers:PersonWindowController 
        x:Key="WindowController" />

</Window.Resources>

...

<ListBox 
    Name="PhonesListBox" 
    Margin="0,25,0,0" 
    HorizontalAlignment="Stretch" 
    VerticalAlignment="Top" 
    ItemsSource="{Binding Path=SelectedPerson.Phones, 
                  Source={StaticResource ResourceKey=WindowController}}"
    HorizontalContentAlignment="Stretch" />

...

In its code behind class, there's a handler for a button which adds a new item to that PhonesListBox:

private void AddPhoneButton_Click(object sender, RoutedEventArgs e)
{
    ObservableCollection<AccountDetail> phones;

    phones = (ObservableCollection<AccountDetail>)PhonesListBox.ItemsSource;
    phones.Add(new AccountDetail(DetailType.Phone));
}

The problem is, the newly added list box item is not added in the person's details observable collection, i.e. the Phones property is not updated (set is never called). Why? Where am I making a mistake?

Thanks for all the help.

UPDATE: I changed the AddPhoneButton_Click method to:

private void AddPhoneButton_Click(object sender, RoutedEventArgs e)
{
    PersonWindowController windowController;
    ObservableCollection<AccountDetail> details;

    windowController = (PersonWindowController)this.FindResource("WindowController");
    details = windowController.SelectedPerson.Details;
    details.Add(new AccountDetail(DetailType.Phone));
}

This updates the appropriate collection, which is details not Phones (as phones is just a view or a getter of a subset of detail items). Also, I realized I don't even need the Phones setter. The problem I am facing now is that my UI is not updated with the changes made to the details collection (and subsequently phones). I don't know how or where to call for the property changed as neither details nor phones are changing; their collection members are. Help. Please.

Upvotes: 0

Views: 3331

Answers (4)

lesscode
lesscode

Reputation: 6361

Try something like:

public class Person : INotifyPropertyChanged
{
    public string FirstName
    {
        get { return firstName; }
        set { firstName = value; }
    }
    public string LastName
    {
        get { return lastName; }
        set { lastName = value; }
    }
    public ObservableCollection<AccountDetail> Details
    {
        get { return details; }
        set { details = value; }
    }

    public void AddDetail(AccountDetail detail) {
        details.Add(detail);
        OnPropertyChanged("Phones");
    }

    public IEnumerable<AccountDetail> Phones
    {
        get
        {
            return details.Where(d => d.Type == DetailType.Phone);
        }
    }

    private string firstName;
    private string lastName;
    private ObservableCollection<AccountDetail> details;


    /// <summary>
    /// Called when a property changes.
    /// </summary>
    /// <param name="propertyName">Name of the property.</param>
    protected void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler propertyChanged = this.PropertyChanged;
        if (propertyChanged != null)
        {
            propertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    #region INotifyPropertyChanged Members

    /// <summary>
    /// Occurs when a property value changes.
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;

    #endregion

}

You could call the AddDetail method from you button event handler:

private void AddPhoneButton_Click(object sender, RoutedEventArgs e)
{
    PersonWindowController windowController;
    ObservableCollection<AccountDetail> details;

    windowController = (PersonWindowController)this.FindResource("WindowController");
    windowController.SelectedPerson.AddDetail(new AccountDetail(DetailType.Phone));
}

Raising the OnPropertyChanged event for the Phones property will simply cause the WPF binding framework to requery the propery, and the Linq query to be re-evaluated, after you added a Phone detail to the list of Details.

Upvotes: 0

Dean Chalk
Dean Chalk

Reputation: 20471

it sounds like you have an ObservableCollection<AccountDetail> with more than just phones in it, so it looks like you actually need a CollectionViewSource with a Filter added:

public class Person
{
    public ObservableCollection<AccountDetail> Details { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}


<Window.Resources>
    <CollectionViewSource x:Key="phonesSource" 
        Source="{StaticResource ResourceKey=WindowController}"
            Path="SelectedPerson.Details"  />
</Window.Resources>
<Grid>
    <ListBox 
        Name="PhonesListBox" 
        Margin="0,25,0,0" 
        HorizontalAlignment="Stretch" 
        VerticalAlignment="Top" 
        ItemsSource="{Binding Source={StaticResource phonesSource}}"
        HorizontalContentAlignment="Stretch" />
</Grid>

public MainWindow()
{
    InitializeComponent();
    CollectionViewSource source = 
        (CollectionViewSource)FindResource("phonesSource");
    source.Filter += (o, e) =>
            {
                if (((AccountDetail) e.Item).Type == DetailType.Phone)
                    e.Accepted = true;
            };
}

Upvotes: 1

decyclone
decyclone

Reputation: 30840

Changing you Person class to something like following should work :

public class Person
{
    public string FirstName
    {
        get { return firstName; }
        set { firstName = value; }
    }
    public string LastName
    {
        get { return lastName; }
        set { lastName = value; }
    }
    public ObservableCollection<AccountDetail> Details
    {
        get { return details; }
        set { details = value; }
    }

    public ObservableCollection<AccountDetail> Phones
    {
        get
        {
            if (phones == null)
            {
                phones = new ObservableCollection<AccountDetail>();
            }

            phones.Clear();
            foreach (AccountDetail detail in Details)
            {
                if (detail.Type == DetailType.Phone)
                {
                    phones.Add(detail);
                }
            }
            return phones;
        }
        set
        {
            phones.Clear();

            foreach (var item in value)
            {
                phones.Add(item);
            }

            foreach (AccountDetail detail in Details)
            {
                if (detail.Type == DetailType.Phone)
                {
                    Details.Remove(detail);
                }
            }

            foreach (AccountDetail detail in phones)
            {
                if (!string.IsNullOrEmpty(detail.Value))
                {
                    Details.Add(detail);
                }
            }
        }
    }

    private string firstName;
    private string lastName;
    private ObservableCollection<AccountDetail> details;
    public ObservableCollection<AccountDetail> phones;
}

The code is not tested and it may require a few changes from you to actually work.

Upvotes: 0

Andy
Andy

Reputation: 30418

Why do you create a new ObservableCollection<AccountDetail> each time the Phones property is retrieved? Typically the Person class would have a member field of type ObservableCollection<AccountDetail> that would just be returned in the getter for the Phones property, instead of creating a new one each time. You could populate this collection when an instance of Person is constructed, for example.

I don't know if this would fix your problem or not, but it seems like it should help.

Upvotes: 2

Related Questions