简体   繁体   中英

Refreshing UI with databind in WPF

I have a 3 layer deep treeview,

-MAIN
 ->:SUB1
   >:SUB2
   >:SUB2
 -X:SUB1
   X:SUB2
 SUB1
 SUB1

where, > and X represent graphics denoting the status of that specific item (determined from backend).

I'm using an Observable Dictionary to bind to this tree (and it has an ICollectionChanged event). The structure is like this:


ObservableDictionary<string,CustomClass> mainitems;

public class CustomClass{
    ObservableDictionary<string, InnerClass> sub1item;
    // Bunch of properties and methods in this class
    // INotify not implemented
}

public class InnerClass{
    // Bunch of properties and methods in this class
    // INotify not implemented
    public SomeEnum Status{
        get{ return this.status; }
    }
}

The graphics, mentioned above, are binded using a custom converter which converts the Status enum to a path so that it can be binded (ie. <img source="{Binding Path=something, Converter={StaticResource someconverter}, Mode=OneWay" /> ).

QUESTION:

My problem is, when I update the CustomClass's sub1item dictionary with new statuses, it doesn't update it in the UI. I think implementing INotify stuff might work but I don't know where I need to update it and exactly how to do so.

Edit: My XAML template for the treeview is as follows:


<TreeView Name="tvInstance" ItemsSource="{Binding}" TreeViewItem.Selected="tviSelected" IsTextSearchEnabled="True">
    <TreeView.ItemContainerStyle>
        <Style>
            <Setter Property="TreeViewItem.IsExpanded" Value="{Binding Path=Value.Expanded, Mode=TwoWay}" />
        </Style>
    </TreeView.ItemContainerStyle>
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate ItemsSource="{Binding Path=Value.CustomClass}" ItemContainerStyle="{x:Null}">
                <StackPanel Orientation="Horizontal">
                <Label Content="{Binding Path=Key}"/>
            </StackPanel>
            <HierarchicalDataTemplate.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding Path=Value.AnotherClass}">
                    <StackPanel Orientation="Horizontal">
                        <Image Source="{Binding Path=Value.Status, Converter={StaticResource convertstatus} }"
                            Width="10" Height="10"/>
                        <Label Content="{Binding Path=Key}" />
                    </StackPanel>
                    <HierarchicalDataTemplate.ItemTemplate>
                        <DataTemplate>
                            <StackPanel Orientation="Horizontal">
                                <Image Source="{Binding Path=Value, Converter={StaticResource convertstatus} }"
                            Width="10" Height="10"/>
                                <Label Content="{Binding Path=Key}" />
                            </StackPanel>
                        </DataTemplate>
                    </HierarchicalDataTemplate.ItemTemplate>
                </HierarchicalDataTemplate>
            </HierarchicalDataTemplate.ItemTemplate>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
</TreeView>

EDIT: After adding all INotifyProperty events in my mainclass, my CustomClass, and my InnerClass, it still doesn't work. I'm using the Dr. WPF version of ObservableDictionary (and using a dictionary is crucial to my application since I need to do lots of lookups). Help!

Epilogue

The answers in this page are correct in that INotifyPropertyChanged needs to be implemented on properties I want updated in the UI.

I found that binding the dictionary was too much trouble so I kept both an ObservableCollection and a Dictionary. I used the dictionary for lookup and the collection for binding (since both use the same reference to the object, removing was easy with the collection and the only O(n) operation).

With regards to updating in the UI, please refer to the other posts on this page.

This may be a little long, here would be my best guess:

public class CustomClass : INotifyPropertyChanged
{
  public CustomClass()
  {
    sub1item = new ObservableDictionary<string, InnerClass>();
    // This next line may not be necessary... Changes might propogate up.
    sub1item.CollectionChanged += () => NotifyPropertyChange("Sub1Item");
  }

  private ObservableDictionary<string, InnerClass> sub1item;
  public ObservableDictionary<string, InnerClass> Sub1Item
  {
    get { return sub1item; }
    private set { sub1item = value; NotifyPropertyChange("Sub1Item"); }
  }

  public event PropertyChangedEventHandler PropertyChanged;

  private void NotifyPropertyChanged(String info)
  {
    if (PropertyChanged != null)
    {
      PropertyChanged(this, new PropertyChangedEventArgs(info));
    }
  }
}

public class InnerClass : INotifyPropertyChanged
{
  public SomeEnum Status
  {
    get { return this.status; }
    private set { this.status = value; NotifyPropertyChange("Status"); }
  }

  public event PropertyChangedEventHandler PropertyChanged;

  private void NotifyPropertyChanged(String info)
  {
    if (PropertyChanged != null)
    {
      PropertyChanged(this, new PropertyChangedEventArgs(info));
    }
  }
}

Just make sure you update your status by calling Status = something, and not directly through this.status

Edit: If you're just looking to ONLY update the single object that got the updated status, I'm not sure that this will do it. I suspect this will signal that Sub1Item changed, but mainitems will likely not know about the individual object. It depends on your implementation.

If you created a DataTemplate for CustomClass, which had a binding to Sub1Item, then your binding will properly update for only the updated status

<DataTemplate DataType="{x:Type myClrNamespace:InnerClass}">
    <Grid>
        <TextBlock Text={Binding Path=Status}/>
    </Grid>
</DataTemplate>
...
<ListBox x:Name="listStatus"/>

Then in the C# somewhere, you could have: listStatus = mainlist[0].Sub1Item; After seeing your example of your TreeView ItemTemplate though, I'm not sure anymore.

Observable collections implement INofityCollectionChanged which is used by WPF to refresh the collection of view items.

However, for the status to be updated you need your data to implement INotifyPropertyChanged .

  • Each class you want to appear within the view must implement it, so WPF will know when its properties change and which of its properties has changed.

The implementation is simple...

// Should implement INotifyPropertyChanged if the dictionary itself
// can be changed and not only its items
public class CustomClass {
    ObservableDictionary sub1item;
    // Bunch of properties and methods in this class
    // INotify not implemented
}


public class InnerClass : INotifyProperyChanged {
    // Bunch of properties and methods in this class
    // INotify not implemented
    public SomeEnum Status{
        get{ return this.status; }
    }

    public event PropertyChangedEventHandler PropertyChanged;



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


// where ever this.status is changed directly,
// call NotifyPropertyChanged("Status")
// (at end of that method)
//
// if this.status is changed from outside class (if public),
// then add a public method NotifyStatusChanged() which calls
// NotifyPropertyChanged("Status")
//
// If Status property has a set{} then if new value != this.status,
// call NotifyPropertyChanged("Status") at end of setter

}

You need to use an event, have your class implement INotifyPropertyChanged, it would look something like this:

public class InnerClass: INotifyPropertyChanged
{
     private string _propertyName;

     //Implemented from INotifyPropertyChanged
     public event PropertyChangedEventHandler PropertyChanged;

     public string PropertyName
     {
        get { return _propertyName; }
        set 
        { 
              _propertyName = value;
              OnPropertyChanged("Name or Property Data"); 
        }
     }

     //Just using string as an example, send whatever data you'd like
     protected void PropertyChanged(string name)
     {
        //Check to make sure the event is wired.
        if(PropertyChanged != null)
        {
              //Fire event
              PropertyChanged(this, name);
        }
     }
}

Basically, have these events fire for your sub items and pass up to your CustomClass object. Then, if need be, have the CustomClass handle these events, and fire another event up to your main object telling it to update the UI.

ObservableDictionary(Of TKey, TValue) - VB.NET

General feature list:

  • ObservableDictionary(Of TKey, TValue)
  • AddRange getting notified only once.
  • Generic EventArgs(Of TKey, TValue)
  • NotifyDictionaryChanging(Of TKey, TValue) - a subclass of CancelEventArgs that allows cancelling operation.

working example for class of type "Task"

public class Task: INotifyPropertyChanged

{

    //Implemented from INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyPropertyChanged(String info)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(info));
        }
    }

private string text;

    public string Text
    {
        get { return text; }
        set { 
            text = value;
            NotifyPropertyChanged("Text");

            }
    }

}

On a side note its worth remembering that you need to use an ObservableCollection rathan than List to get a dynamically updating ItemSource when databinding to a collection of types. List does not notify.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM