Binding to count on observable collection of subclass - c#

I do have a WPF binding question here.
Following Setup:
I do have a class (ActionService) having a name and a ObservableCollection of subitems (also a class named Step). A Step has a flag that shows if the Step is allready done (IsDone).
I bind a form to the ActionService and display all kind of things.
Everything works as aspected and i have just the essential parts in my snippet.
Now I need one more thing that i can not get work. I want the ActionService to know by binding how many of its Steps are open (IsDone == false). I you open a childform with one of the steps and change the IsDone-State, the mother form should get the new count on the fly.
And I'm to dumb to get a correct solution on the way ;-)
Thanks for your help or a best practise.
public class ActionService : BaseObject
{
public ActionService()
{
}
private String name;
public String Name
{
get { return this.name; }
set
{
this.name = value;
raisePropertyChanged("Name");
}
}
public ObservableCollection<Step> actionsteps;
public ObservableCollection<Step> ActionSteps
{
get { return this.actionsteps; }
set
{
this.actionsteps = value;
raisePropertyChanged("ActionSteps");
}
}
}
public class Step : BaseObject
{
public Step()
{
}
private String description;
public String Description
{
get { return this.description; }
set
{
this.description = value;
raisePropertyChanged("Description");
}
}
private Boolean isdone;
public Boolean IsDone
{
get { return this.isdone; }
set
{
this.isdone = value;
raisePropertyChanged("IsDone");
}
}
}
public class BaseObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void raisePropertyChanged(String parPropertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(parPropertyName));
}
}
}

You can create a new property in your ActionService class:
public bool IsDone
{
get
{
return ActionSteps.Count(x => x.IsDone) == ActionSteps.Count;
}
}
If the count of Steps in the ActionSteps list where the IsDone property is true is equal to the number of Steps in the ActionSteps list, then return true, else, return false.
To subscribe to the Steps property changed event, when you add an item to the collection, you simply need to subscribe to the PropertyChanged event:
//Create the item and subscribe to propertychanged.
Step item = new Step();
item.PropertyChanged += item_PropertyChanged;
//Add the item to the list.
ActionSteps.Add(item);
And your method will look like this:
void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "IsDone")
raisePropertyChanged("IsDone");
}

Related

Changes to collection do not update UI

I don't understand why when I update a object, my bound controls do not update.
The data displays fine initially, but when I want to refresh the data displayed in the UI nothing happens when I update the object. The object updates fine. The ViewModel does use INotifyPropertyChanged on all fields.
However if I update individual items directly, I can update my UI. As commented below.
I guess I've made a school boy error somewhere here?
UPDATE: I've added the model to the question. While I understand the answers, I don't understand how to implement it. Attempted to implement a collection changed event without success. Can I have some pointers please?
public partial class CisArrivalsPanel : UserControl
{
private ApiDataArrivalsDepartures _theArrivalsDepartures;
public CisArrivalsPanel()
{
InitializeComponent();
_theArrivalsDepartures = new ApiDataArrivalsDepartures();
_theArrivalsDepartures = MakeQuery.LiveTrainArrivals("London Kings Cross");
this.DataContext = _theArrivalsDepartures;
ListBoxArr.ItemsSource = _theArrivalsDepartures.StationMovementList;
}
void Reload()
{
//This does not update the UI**
_theArrivalsDepartures = MakeQuery.LiveTrainArrivals("London Paddington");
//However this (when uncommented, and I comment out the above line) does update the UI**
//_theArrivalsDepartures.StationMovementList[0].OriginName = "test";
//_theArrivalsDepartures.StationMovementList[0].Platform = "0";
//_theArrivalsDepartures.StationMovementList[0].BestArrivalEstimateMins = "999";
//_theArrivalsDepartures.StationName = "test";
}
private void StationHeader_OnPreviewMouseDown(object sender, MouseButtonEventArgs e)
{
Reload();
Debug.WriteLine(_theArrivalsDepartures.StationName);
foreach (var a in _theArrivalsDepartures.StationMovementList)
{
Debug.WriteLine(a.OriginName);
Debug.WriteLine(a.BestArrivalEstimateMins);
}
}
}
EDIT : Added Model
public class ApiDataArrivalsDepartures : INotifyPropertyChanged
{
private string _stationName;
[JsonProperty(PropertyName = "station_name")]
public string StationName {
get
{
return _stationName;
}
set
{
_stationName = value;
NotifyPropertyChanged("StationName");
}
}
private List<StationListOfMovements> _stationMovementList;
public List<StationListOfMovements> StationMovementList
{
get
{
return _stationMovementList;
}
set
{
_stationMovementList = value;
NotifyPropertyChanged("StationMovementList");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
public class StationListOfMovements : INotifyPropertyChanged
{
private string _originName;
[JsonProperty(PropertyName = "origin_name")]
public string OriginName {
get
{
return _originName;
}
set
{
_originName = value;
NotifyPropertyChanged("OriginName");
}
}
[JsonProperty(PropertyName = "destination_name")]
public string DestinationName { get; set; }
private string _platform;
[JsonProperty(PropertyName = "Platform")]
public string Platform {
get
{
return _platform;
}
set
{
_platform = value;
NotifyPropertyChanged("Platform");
}
}
private string _bestArrivalEstimateMins;
[JsonProperty(PropertyName = "best_arrival_estimate_mins")]
public string BestArrivalEstimateMins {
get
{
return _bestArrivalEstimateMins;
}
set
{
_bestArrivalEstimateMins = value;
NotifyPropertyChanged("BestArrivalEstimateMins");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
There are two pieces here pertaining to your collection (technically three):
If you want a new collection to propagate, the collection property has to raise PropertyChanged (sounds like it does)
If you want add/remove on the collection to propagate, you need to use a collection that implements INotifyCollectionChanged. ObservableCollection is a good choice.
If you want changes to the items in the container to propagate, then those items need to implement INotifyPropertyChanged and raise the PropertyChanged event.
Make sure all those are covered, and the changes should appear on the UI as you expect.
You should update the DataContext and ItemsSource too.
void Reload()
{
//This does not update the UI**
_theArrivalsDepartures = MakeQuery.LiveTrainArrivals("London Paddington");
DataContext = theArrivalsDepartures;
ListBoxArr.ItemsSource = _theArrivalsDepartures.StationMovementList;
}
Use for the collection ObservableCollection , this class notify the ui when change to the collection occurred
your reload function works because the there is PropertyChanged on all the fields include this one
it notify the ui and reload the correct collection

Refresh an ObservableCollection (overwrite)

How can I refresh the following ObservableCollection?
public class ViewModelProperties
{
private ObservableCollection<ServerProperties> properties;
public ObservableCollection<ServerProperties> Properties
{
get
{
properties = new ObservableCollection<ServerProperties>();
for (var lineNumber = 0; lineNumber < MainWindow.lineCount; lineNumber++)
{
if (MainWindow.textProperties[lineNumber, 0] == null) break;
properties.Add(new ServerProperties(MainWindow.textProperties[lineNumber, 0],
MainWindow.textProperties[lineNumber, 1]));
}
return properties;
}
}
}
public class ServerProperties
{
private string property;
private string value;
public ServerProperties()
{
}
public ServerProperties(string property, string value)
{
Property = property;
Value = value;
}
public string Property
{
get
{
return this.property;
}
set
{
this.property = value;
}
}
public string Value
{
get
{
return this.value;
}
set
{
this.value = value;
}
}
public override string ToString()
{
return string.Format("[Property : {0}]", Value);
}
}
I changed the value of textProperties[,] and now I'd like to overwrite the previous content of the collection with the current content of textProperties[,].
What would be the simplest way to do this?
Any help would be appreciated.
Start off by implementing INotifyPropertyChanged in your ViewModel as well as in the ServerProperties object. This way you can raise the PropetyChanged event which will pass back to the user interface.
ViewModel
public class ViewModelProperties : INotifyPropertyChanged {
public event ProeprtyChangedEventHandler PropertyChanged;
private ObservableCollection<ServerProperties> properties = new ObservableCollection<ServerProperties>();
public ObservableCollection<ServerProperties> Properties {
get { return properties;}
set {
properties = value;
this.RaisePropertyChangedEvent("Properties");
}
}
private void RaisePropertyChanged(string propertyName) {
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Implementing this on the ServerProperties object as well will allow you to change the objects, at any level, and have it bubble up to the interface.
Lastly look into your population code and in order to get the property to update successfully first populate it to a List then re-initialise the ObservableCollection using the List.
Properties = new ObservableCollection<ServerProperties>(propertiesList);
This also allows you to better handle the creation of your ObservableCollection and perform tests before posting the output to the interface. Hope it helps.
For example, one of the simpler solutions could be
public class ViewModelProperties
{
private ObservableCollection<ServerProperties> properties = new ObservableCollection<ServerProperties>();
public ObservableCollection<ServerProperties> Properties
{
get
{
return properties;
}
}
public void SetProperties()
{
properties.Clear();
for (var lineNumber = 0; lineNumber < MainWindow.lineCount; lineNumber++)
{
if (MainWindow.textProperties[lineNumber, 0] == null) break;
properties.Add(new ServerProperties(MainWindow.textProperties[lineNumber, 0],
MainWindow.textProperties[lineNumber, 1]));
}
}
}
Any time you wish to add new items to OC, just call the SetProperties method.

Silverlight Binding - Binds when item is added but doesn't get updates

I'm sorta at a loss to why this doesn't work considering I got it from working code, just added a new level of code, but here's what I have. Basically, when I bind the ViewModel to a list, the binding picks up when Items are added to a collection. However, if an update occurs to the item that is bound, it doesn't get updated. Basically, I have an ObservableCollection that contains a custom class with a string value. When that string value gets updated I need it to update the List.
Right now, when I debug, the list item does get updated correctly, but the UI doesn't reflect the change. If I set the bound item to a member variable and null it out then reset it to the right collection it will work, but not desired behavior.
Here is a mockup of the code, hopefully someone can tell me where I am wrong. Also, I've tried implementing INofityPropertyChanged at every level in the code below.
public class Class1
{
public string ItemName;
}
public class Class2
{
private Class2 _items;
private Class2() //Singleton
{
_items = new ObservableCollection<Class1>();
}
public ObservableCollection<Class1> Items
{
get { return _items; }
internal set
{
_items = value;
}
}
}
public class Class3
{
private Class2 _Class2Instnace;
private Class3()
{
_Class2Instnace = Class2.Instance;
}
public ObservableCollection<Class1> Items2
{
get {return _Class2Instnace.Items; }
}
}
public class MyViewModel : INofityPropertyChanged
{
private Class3 _myClass3;
private MyViewModel()
{
_myClass3 = new Class3();
}
private BindingItems
{
get { return _myClass3.Items2; } // Binds when adding items but not when a Class1.ItemName gets updated.
}
}
The answer to your question is that Class1 needs to implement INotifyPropertyChanged.
public class Class1 : INotifyPropertyChanged
{
private string _ItemName;
public string ItemName
{
get { return _ItemName; }
set
{
_ItemName = value;
NotifyPropertyChanged("ItemName");
}
}
private void NotifyPropertyChanged(string name)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
public event PropertyChangedEventHandler PropertyChanged;
}

BindingSource with Generic SubClass in Windows Forms

I'm attempting to do what I considered simple data binding between a BindingSource and a ComboBox. I run into issues when the class I am using as the DataSource of the BindingSource has a property that is an instance of a generic class.
I have the following generic class:
public class GenericClass<T>
{
public T Code { get; set; }
public string Description { get; set; }
public override string ToString()
{
return Description;
}
}
I have a class that has an integer Code:
public class IntegerClass : GenericClass<int>
{
// Nothing unique here, for simple test.
}
I also have the class that is set to the BindingSource's DataSource:
public class ClassBindingClass : INotifyProperty Changed
{
private int _id;
private IntegerClass _choice;
private string _name;
public int Id
{
get { return _id; }
set
{
_id = value;
OnPropertyChanged("Id");
}
}
public IntegerClass Choice
{
get { return _choice; }
set
{
_choice = value;
OnPropertyChanged("Choice");
}
}
public string Name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged("Name");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertName));
}
}
On my form I create a collection of IntegerClass and set my combobox's datasource as that collection. (This part works fine, the combo box displays the values appropriately.) Then I set the combobox's SelectedValue Binding to the BindingSource's Choice property updating on OnPropertyChanged.
If I replace IntegerClass with a non-generic class when you select a value in the combo box the BindingSource's Choice property changes the NotifyPropertyChanged event is fired and on my form I can update a label saying "Choice has changed!".
When the IntegerClass is part of the ClassBindingClass this no longer works and instead I cannot navigate out of the combo box and instead get a FormatException.
Is what I want to do possible? Can databinding handle generics?
You mention SelectedValue... but your source (and the bound property) are both IntegerClass - so it isn't a value you want to bind, but the item itself. Unfortunately, there is no ComboBox.SelectedItemChanged so you might need to hack it a bit to get 2-way binding...
static class Program {
[STAThread]
static void Main() {
Application.EnableVisualStyles();
IntegerClass[] choices = new[] {
new IntegerClass { Code = 123, Description = "a b c"},
new IntegerClass { Code = 456, Description = "d e f"},
new IntegerClass { Code = 789, Description = "g h i"},
};
ComboBox cbo = new TwoWayComboBox();
cbo.DropDownStyle = ComboBoxStyle.DropDownList;
cbo.DataSource = choices;
Form form = new Form();
ClassBindingClass obj = new ClassBindingClass();
cbo.DataBindings.Add("SelectedItem", obj, "Choice", true, DataSourceUpdateMode.OnPropertyChanged);
form.DataBindings.Add("Text", obj, "Choice", true, DataSourceUpdateMode.OnPropertyChanged); // show it
form.Controls.Add(cbo);
Application.Run(form);
}
}
class TwoWayComboBox : ComboBox {
public new object SelectedItem
{
get { return base.SelectedItem; }
set { base.SelectedItem = value; }
}
private static readonly object SelectedItemChangedKey = new object();
public event EventHandler SelectedItemChanged {
add { Events.AddHandler(SelectedItemChangedKey, value);}
remove { Events.RemoveHandler(SelectedItemChangedKey, value);}
}
protected override void OnSelectedIndexChanged(EventArgs e)
{
EventHandler handler = (EventHandler)Events[SelectedItemChangedKey];
if (handler != null) { handler(this, EventArgs.Empty); }
base.OnSelectedIndexChanged(e);
}
}

how to update listbox items with INotifyPropertyChanged

I have a listbox which is databound to a collection of objects.
I want to modify the way the items are displayed to show the user which one of these objects is the START object in my program.
I tried to do this the following way, but the listbox does not automatically update.
Invalidating the control also didn't work.
The only way I can find is to completely remove the databindings and add it back again. but in my case that is not desirable.
Is there another way?
class Person : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _name;
public string Name
{
get
{
if (PersonManager.Instance.StartPerson == this)
return _name + " (Start)";
return _name;
}
set
{
_name = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Name"));
}
}
public Person(string name)
{
Name = name;
}
}
This is the class wich manages the list and the item that is the start
class PersonManager
{
public BindingList<Person> persons { get; set; }
public Person StartPerson { get; set; }
private static PersonManager _instance;
public static PersonManager Instance
{
get
{
if (_instance == null)
{
_instance = new PersonManager();
}
return _instance;
}
}
private PersonManager()
{
persons = new BindingList<Person>();
}
}
In the form I use the following code
private void button1_Click(object sender, EventArgs e)
{
PersonManager.Instance.StartPerson = (Person)listBox1.SelectedItem;
}
I'm pretty sure that the problem is that, when you do this, you're effectively making the Person.Name properties "get" accessor change the value (and act like a set accessor as far as the UI is concerned).
However, there is nothing that's updating the bindings to say that this is happening. If PropertyChanged got called when you set start, I believe this would update.
It's clunky, but the way you have it written, I believe you could add this and make it work (NOTE: I didn't test this, so it ~may~ have issues):
private void button1_Click(object sender, EventArgs e)
{
Person newStart = (Person)listBox1.SelectedItem;
if (newStart != null)
{
PersonManager.Instance.StartPerson = newStart;
newStart.Name = newStart.Name; // Dumb, but forces a PropertyChanged event so the binding updates
}
}

Categories

Resources