1

In the following binding,

<TextBlock Text="{Binding Path=., StringFormat=TotalPages:{0}, Converter={StaticResource totalPagesConverter}}"/>

the totalPagesConverter takes ObservableCollection<MyFileInfo> and returns the total number of pages.

This works only for the first time, but doesn't not update when a MyFileInfo object's property is changed. Note that I did implement INotifyPropertyChanged for MyFileInfo, and things update properly when they are binded to a Property of MyFileInfo. But apparently something is missing when binding to a Collection of such objects. How to bind to a Collection so that it updates properly? Thanks!


UPDATE Thank you all! properties of MyFileInfo and the converter are shown below:

MyFileInfo class

public class MyFileInfo : INotifyPropertyChanged
{
    private Boolean ifPrint;
    private Boolean isValid;

    public string Filename {
        get; set;
    }

    public string Filepath {
        get; set;
    }

    public int Copies {
        get; set;
    }

    public int Pages {
        get; set;
    }

    public Boolean IfPrint {
        get{
            return this.ifPrint;
        }
        set{
            if (this.ifPrint != value){
                this.ifPrint = value;
                onPropertyChanged("IfPrint");
            }
        }
    }

    public Boolean IsValid {
        get {
            return this.isValid;
        }
        set {
            if (this.isValid!= value) {
                this.isValid = value;
                onPropertyChanged("IsValid");
            }
        }
    }

    private void onPropertyChanged(string propertyName) {
        if (PropertyChanged != null) {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

Converter Class

public class TotalPagesConverter : IValueConverter {

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
        ObservableCollection<MyFileInfo> lst = (ObservableCollection<MyFileInfo>)value;
        int t = 0;
        foreach(MyFileInfo f in lst){
            t += (f.IsValid && f.IfPrint) ? f.Pages : 0;
        }

        return t.ToString();
    }


    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
        throw new NotImplementedException();
    }
}

A bit on the Converter: I have a CheckBox for the user to choose files to be printed. Everytime the CheckBox is toggled, the IfPrint boolean property of an MyFileInfo object is flipped. This Converter goes through all the IfPrint && IsValid files to re-compute the total number of pages, and update the new total to-be-printed pages on GUI (ideally). This isn't that efficient, but the list is short (< 10). Any other idea is welcomed!

totoro
  • 3,257
  • 5
  • 39
  • 61
  • show the collection property code – thumbmunkeys Oct 29 '14 at 10:17
  • 3
    Binding to . binds to the object itself, so you will only get notifications when the object changes. Since the observable collection does not change, it will not receive notifications. – sondergard Oct 29 '14 at 10:23
  • Call OnPropertyChanged("FileInfos"); explicitly whenever you perform add or remove operation on your observableCollection and then it would update the textbox whenever your count changes – Krishna Oct 29 '14 at 11:09

3 Answers3

2

This works only for the first time, but doesn't not update when a MyFileInfo object's property is changed.

An ObservableCollection will only raise a notification event when the collection itself changes, such as adding or removing an item.

If you want the collection to also fire an "I have changed" message whenever any of the items inside the collection changes, you'll have to hook up property changed handlers for that yourself.

public MyViewModel()
{
    FileInfoCollection = new ObservableCollection<MyFileInfo>();

    // Hook up initial changed handler. Could also be done in setter
    FileInfoCollection.CollectionChanged += FileInfoCollection_CollectionChanged;
}

void FileInfoCollection_CollectionChanged(object sender, CollectionChangedEventArgs e)
{
    if (e.NewItems != null)
    {
        foreach(MyFileInfo item in e.NewItems)
        {
            item.PropertyChanged += MyFileInfo_PropertyChanged;
        }
    }

    if (e.OldItems != null)
    {
        foreach(MyFileInfo item in e.OldItems)
        {
            item.PropertyChanged -= MyFileInfo_PropertyChanged;
        }
    }
}

void MyFileInfo_PropertyChanged(object sender, PropertyChange e)
{
    // Whenever a FileInfo changes, raise change notification for collection
    RaisePropertyChanged("FileInfoCollection");
}

That said, I don't understand why a binding for the count of items in a collection needs to be re-evaluated whenever a property of an item inside the collection changes, unless you're doing some kind of filtering inside your converter.

Also, if you're just binding to the count of the collection, can't you just bind to the .Count property on ObservableCollection<T>?

<TextBlock Text="{Binding Path=Count, StringFormat=TotalPages:{0}}" />
Rachel
  • 130,264
  • 66
  • 304
  • 490
  • i have this code `public ObservableCollection fileLst = new ObservableCollection();`, but `fileLst` doesn't seem to have the property `CollectionChanged`. this is in codebehind. Does your solution must be in a `ViewModel`? (haven't learned that yet...) – totoro Oct 30 '14 at 02:34
  • @green That's strange, `ObservableCollection` has a [CollectionChanged](http://msdn.microsoft.com/en-us/library/ms653375(v=vs.110).aspx) event, so I'm not sure why you wouldn't be seeing it. – Rachel Oct 31 '14 at 02:49
  • thanks again for your answer. now i am learning to redo this in MVVM, and starts to better understand your solution. would you explain the purpose of `+=` and '-='? – totoro Nov 01 '14 at 08:19
  • 1
    @green Sure, the `item.PropertyChanged += MyFileInfo_PropertyChanged` attaches a change event handler to each item added to the collection, and `item.PropertyChanged -= MyFileInfo_PropertyChanged` makes sure the change handler is removed from each item when it gets removed from the collection. – Rachel Nov 01 '14 at 19:59
1

If I understand correctly, you list the items in some sort of list/grid, and each item has a checkbox bound to the IfPrint property. When the user clicks the checkbox, you want the textblock to update and show the number of MyFileInfos which have the IfPrint and IsValid set to true, correct?

Bindings only listen to PropertyChanged events on the object which is its DataContext. This means that the only controls which will react to IfPrint change is the checkbox. The only way for any other control to react, is to manually communicate the change - the easiest way is to create an event:

public class MyFileInfo : INotifyPropertyChanged
{
    private Boolean ifPrint;
    private Boolean isValid;

    ...

    public Boolean IfPrint {
        get{
            return this.ifPrint;
        }
        set{
            if (this.ifPrint != value){
                this.ifPrint = value;
                onPropertyChanged("IfPrint");

                OnCanPrintChanged();
            }
        }
    }

    public Boolean IsValid {
        get {
            return this.isValid;
        }
        set {
            if (this.isValid!= value) {
                this.isValid = value;
                onPropertyChanged("IsValid");

                OnCanPrintChanged();
            }
        }
    }

    private void onPropertyChanged(string propertyName) {
        if (PropertyChanged != null) {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    private void OnCanPrintChanged()
    {
        if (CanPrintChanged != null)
        {
            CanPrintChanged(this, EventArgs.Empty);
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    public event EventHandler CanPrintChanged;
}

In your code behind class you hook up to this event for each MyFileInfo in the list, and update a property in your codebehind/viewmodel:

public class MyCodeBehind : INotifyPropertyChanged
{
    private int _printablePageCount = 0;

    public int PrintablePageCount
    {
        get { return _printablePageCount; }
        set 
        { 
            return _printablePageCount = value; 
            OnPropertyChanged("PrintablePageCount");
        }
    }

    private void OnCanPrintChanged(object sender, EventArgs arg)
    {
        int t = 0;
        foreach(MyFileInfo f in lst)
        {
            if (f.IsValid && f.IfPrint)
            {
                t++;
            }
        }

        PrintablePageCount = t;
    }


    private void OnPropertyChanged(string propertyName) 
    {
        if (PropertyChanged != null) 
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

And in your XAML you bind to your PrintablePageCount instead (without the need for a converter):

<TextBlock Text="{Binding Path=PrintablePageCount, StringFormat=TotalPages:{0}}"/>
sondergard
  • 3,184
  • 1
  • 16
  • 25
  • thanks, worked for me! would you add the code for registering the `EventHandler` to the method in codebehind? it might not be obvious to a newbie like me (took me a while to figure it out). thanks again! – totoro Oct 31 '14 at 02:21
  • As opposed to in a ViewModel? No if you are working with view models, the MyFileInfo classes would be loaded in the viewmodel, and the event listener would be added here as well. It would still work exactly the same, because the PrintablePageCount property would be on the VM, and TextBlock would bind to this property in exactly the same way. – sondergard Oct 31 '14 at 09:14
  • 1
    Ok, I am not sure which method you are asking about adding the event handler in, but just to be sure: Add the event handler to each MyFileInfo after loading them, so you are only adding it only once. Also remember to remove them again before leaving the view (changing page or whatever), so you are not leaking event handlers. – sondergard Oct 31 '14 at 10:51
  • +1 for mentioning memory leak due to handlers. from reading [this](http://stackoverflow.com/questions/4526829/why-and-how-to-avoid-event-handler-memory-leaks), it seems okay to not remove the handlers if `MyFileInfo` only lives one extra `Page` than the current page. Is my understanding correct? – totoro Nov 01 '14 at 03:37
  • It depends entirely on the lifetime of Page1. If you are sure that Page1 has a lifetime shorter or equal to the MFIs, you should be ok. If you are uncertain, test it out, and remember sometimes it's also good to be on the safe side - this prevents future memory leaks if the code changes. Also remember that the potential memory leak can be quite catastrophic when adding event handlers in a loop - you could end up leaking 1000s of objects. – sondergard Nov 01 '14 at 11:27
0

Don't forget to raise OnPropertyChanged event while setting value of ObservableCollection property.

public ObservableCollection<MyFileInfo> FileInfos
{
 get{return fileInfos;}
 set
 {
   if(fileInfos != value)
   {
     fileInfos = value;
     OnPropertyChanged("FileInfos");
   }
}} 

Add Mode= TwoWay to your binding.

<TextBlock Text="{Binding Path=FileInfos, Mode=TwoWay, StringFormat=TotalPages:{0}, Converter={StaticResource totalPagesConverter}}"/>
Abdurrahman Alp Köken
  • 1,236
  • 1
  • 13
  • 12
  • TwoWay won't make a difference in this case. It's a textblock, so there will never be a view->viewmodel direction. – sondergard Oct 29 '14 at 10:27
  • We are just assuming somethings. Why don't you share collection property code? By the way yes, you're right. I saw it as a TextBox instead of TextBlock :) – Abdurrahman Alp Köken Oct 29 '14 at 10:38