2

I'm creating a custom control in WPF. I bind a List<IMyInterface> to a dependency property. This in turn binds again to a ListBox which shows all the items as expected.

I now want to bind 1 item from this list to a Textblock, so I bind the entire list to the textblock. I have a converter in this which extracts the single item I want.

It has worked fine but for a few reasons, I want to use ObservableCollection instead of List

Oddly, when I change a value in my ObservabaleCollection at run time, the value is shown in the ListBox (success) but not in my textblock. The converter is not even hit!

    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = this;
        this.Errors = new ObservableCollection<IEventDetail>();
        this.Errors.CollectionChanged += Errors_CollectionChanged;
        var bw = new BackgroundWorker();
        bw.DoWork += ((o, e) =>
        {
            System.Threading.Thread.Sleep(1500);
            Dispatcher.Invoke(() =>
            {
                this.Errors.Add(new MyEvents("example of some detail", "Failed title"));
            });

            System.Threading.Thread.Sleep(2500);

            Dispatcher.Invoke(() =>
            {
                this.Errors.Add(new MyEvents("Another example", "Failed title 2"));
            });
        });
        bw.RunWorkerAsync();//background worker for testing/debugging only
    }

    private void Errors_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        OnPropertyChanged("Errors");
    }

    private ObservableCollection<IEventDetail> _errors;
    public ObservableCollection<IEventDetail> Errors
    {
        get
        {
            return this._errors;
        }
        set
        {
            this._errors = value;
            OnPropertyChanged("Errors");
        }
    }


    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged == null)
            return;

        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

And the xaml is simply

 <local:Notify Events="{Binding Errors}" DockPanel.Dock="Right"/>

As you can see, I've tried to use the CollectionChanged event to then force fire the INotifyPropertyChanged, but it's firing in my Converter yet the ListBox is updating fine (so I know the binding is fine)

This is the UserControls xaml

 <TextBlock Text="{Binding Path=Events, RelativeSource={RelativeSource AncestorLevel=1, AncestorType=UserControl}, Mode=Default, Converter={StaticResource MostRecentConverter}}" Grid.Row="0" />

    <ListBox ItemsSource="{Binding Path=Events, RelativeSource={RelativeSource AncestorLevel=1,AncestorType=UserControl}, Mode=Default}" Grid.Row="1">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding EventTitle}" Style="{StaticResource txtBckRed}"/>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>

Do I need to do something else?

Dave
  • 8,163
  • 11
  • 67
  • 103
  • The problem must be on xaml, onpropertychanged is implicit in observableCollection. (cany you post the xaml?) try to set explicti two way binding and UpdataSourceTrigger = PropertyChanged – Xilmiki Apr 16 '16 at 08:06
  • @Xilmiki, thanks but, the binding **is** working on the `ListBox`, but not the `Textblock`. So I'm not sure why I'd need 2 way binding or to even use `UpdateSourceTrigger` – Dave Apr 16 '16 at 08:09
  • Plese try to set explicit two way binding and UpdataSourceTrigger = PropertyChanged in TextBox Binding, i had the same problem on my WPF project. – Xilmiki Apr 16 '16 at 08:14
  • I have, it doesn't help. How would `UpdateSourceTrigger ` help as I'm changing the value in the code behind? And it's not binding 2 ways? – Dave Apr 16 '16 at 08:17
  • 1
    @Dave it `TextBlock` it bound only to `Events` property change (not its items) so it won't trigger unless you create new collection is created. Even manually raising property changed event for `Errors` won't work as UI will see that collection instance is actually the same – dkozl Apr 16 '16 at 08:36
  • @dkozl, right... yes. This makes sense, initially I was creating a brand new List<> each time for this reason. I was hoping my 'hack' of using the `CollectionChanged` would suffice. But, if this is the case, why does it update correctly with the `ListBox`? – Dave Apr 16 '16 at 09:08
  • 1
    Reacting to `INotifyCollectionChanged` is characteristic to `ItemsSource` property. What does your converter do? You could create your collection type based on `ObservableCollection` that adds custom property that does what converter would do or in user control subscribe to `CollectionChanged` event and manually refresh binding expression for `TextBlock` – dkozl Apr 16 '16 at 09:42
  • @dkozl, the converter simply takes the `List` as a parameter, and sorts one of the `IMyInterface` properties (which is `DateTime`). It returns the most recent as a string. Since it doesn't appear possible with the approach I've taken, please do move your comment to an answer? – Dave Apr 16 '16 at 10:53

1 Answers1

1

As mentioned in the comments TextBlock it bound only to Events property change (not its items) so it won't trigger unless new instance of collection is created. Reacting to INotifyCollectionChanged is characteristic to ItemsSource property.

Solution 1

Leave everything as it is at the moment just give TextBlock some name

<TextBlock Text="{Binding ...}" x:Name="myTextBlock"/>

and subscribe to CollectionChanged event inside your UserControl where you manually force binding target to update

public partial class MyUserControl : UserControl
{
    public static readonly DependencyProperty EventsProperty =
                               DependencyProperty.Register("Events", 
                                   typeof(IEnumerable), 
                                   typeof(MyUserControl), 
                                   new PropertyMetadata(EventsPropertyChanged));

    private static void EventsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((MyUserControl)d).EventsPropertyChanged(e);
    }

    private void EventsPropertyChanged(DependencyPropertyChangedEventArgs args)
    {
        var newCollection = args.NewValue as INotifyCollectionChanged;
        if (newCollection != null)
            newCollection.CollectionChanged += (s, e) => myTextBlock.GetBindingExpression(TextBlock.TextProperty).UpdateTarget();
    }

    public IEnumerable Events
    {
        get { return (IEnumerable)GetValue(EventsProperty); }
        set { SetValue(EventsProperty, value); }
    }

}

Solution 2

Create your own collection class inherited from ObservableCollection<T> with custom property that would do what your converter does

public class MyObservableCollection<T> : ObservableCollection<T>
{
    private string _convertedText;

    protected override void OnCollectionChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        base.OnCollectionChanged(e);
        this.ConvertedText = ...; // <- do here what your IValueConverter does
    }

    public string ConvertedText
    {
        get { return _convertedText; }
        private set
        {
            _convertedText = value;
            OnPropertyChanged(new PropertyChangedEventArgs("ConvertedText"));
        }
    }
}

and bind TextBlock.Text to Events.ConvertedText property instead, without need for converter

dkozl
  • 32,814
  • 8
  • 87
  • 89