I can't seem to find a simple, concrete explanation of how to bind controls in a WinForms app to nested objects using data binding. For example:
class MyObject : INotifyPropertyChanged
{
private string _Name;
public string Name
{
get { return _Name; }
set
{
_Name = value;
OnPropertyChanged("Name");
}
}
private MyInner _Inner;
public MyInner Inner
{
get { return _Inner; }
set
{
_Inner = value;
OnPropertyChanged("Inner");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
class MyInner : INotifyPropertyChanged
{
private string _SomeValue;
public string SomeValue
{
get { return _SomeValue; }
set
{
_SomeValue = value;
OnPropertyChanged("SomeValue");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Now imagine a form with just two textboxes, the first for Name and the second for Inner.SomeValue. I'm easily able to get binding to work against Name, but Inner.SomeValue is flaky. If I populate the object and then set up the binding, it shows Inner.SomeValue in the textbox but I can't edit it. If I start from a fresh object without initializing Inner, I can't seem to get data to stick in Inner.SomeValue.
I've checked all over MSDN, all over StackOverflow, and dozens of searches with different keywords. Everyone wants to talk about binding to databases or DataGrids, and most examples are written in XAML.
Update: I've tried Marc's full test harness and have partial success. If I hit the "all change!" button, I seem to be able to write back to the inner object. However, starting with MyObject.Inner null, it doesn't know how to create an inner object. I think for now, I can work around it by just making sure my inner references are always set to a valid object. Still, I can't help feeling like I'm missing something :)
Hmm - an excellent question; I've done lots of data-binding to objects, and I would have sworn that what you are doing should work; but indeed it is very reluctant to notice the change to the inner object. I've managed to get it working by:
var outer = new BindingSource { DataSource = myObject };
var inner = new BindingSource(outer, "Inner");
txtName.DataBindings.Add("Text", outer, "Name");
txtSomeValue.DataBindings.Add("Text", inner, "SomeValue");
Not ideal, but it works. Btw; you might find the following utility methods useful:
public static class EventUtils {
public static void SafeInvoke(this EventHandler handler, object sender) {
if(handler != null) handler(sender, EventArgs.Empty);
}
public static void SafeInvoke(this PropertyChangedEventHandler handler,
object sender, string propertyName) {
if(handler != null) handler(sender,
new PropertyChangedEventArgs(propertyName));
}
}
Then you can have:
class MyObject : INotifyPropertyChanged
{
private string _Name;
public string Name { get { return _Name; } set {
_Name = value; PropertyChanged.SafeInvoke(this,"Name"); } }
private MyInner _Inner;
public MyInner Inner { get { return _Inner; } set {
_Inner = value; PropertyChanged.SafeInvoke(this,"Inner"); } }
public event PropertyChangedEventHandler PropertyChanged;
}
class MyInner : INotifyPropertyChanged
{
private string _SomeValue;
public string SomeValue { get { return _SomeValue; } set {
_SomeValue = value; PropertyChanged.SafeInvoke(this, "SomeValue"); } }
public event PropertyChangedEventHandler PropertyChanged;
}
And in the bargain it fixes the (slim) chance of a null-exception (race-condition).
Full test rig, to iron out kinks (from comments):
using System;
using System.ComponentModel;
using System.Windows.Forms;
public static class EventUtils {
public static void SafeInvoke(this PropertyChangedEventHandler handler, object sender, string propertyName) {
if(handler != null) handler(sender, new PropertyChangedEventArgs(propertyName));
}
}
class MyObject : INotifyPropertyChanged
{
private string _Name;
public string Name { get { return _Name; } set { _Name = value; PropertyChanged.SafeInvoke(this,"Name"); } }
private MyInner _Inner;
public MyInner Inner { get { return _Inner; } set { _Inner = value; PropertyChanged.SafeInvoke(this,"Inner"); } }
public event PropertyChangedEventHandler PropertyChanged;
}
class MyInner : INotifyPropertyChanged
{
private string _SomeValue;
public string SomeValue { get { return _SomeValue; } set { _SomeValue = value; PropertyChanged.SafeInvoke(this, "SomeValue"); } }
public event PropertyChangedEventHandler PropertyChanged;
}
static class Program
{
[STAThread]
public static void Main() {
var myObject = new MyObject();
myObject.Name = "old name";
// optionally start with a default
//myObject.Inner = new MyInner();
//myObject.Inner.SomeValue = "old inner value";
Application.EnableVisualStyles();
using (Form form = new Form())
using (TextBox txtName = new TextBox())
using (TextBox txtSomeValue = new TextBox())
using (Button btnInit = new Button())
{
var outer = new BindingSource { DataSource = myObject };
var inner = new BindingSource(outer, "Inner");
txtName.DataBindings.Add("Text", outer, "Name");
txtSomeValue.DataBindings.Add("Text", inner, "SomeValue");
btnInit.Text = "all change!";
btnInit.Click += delegate
{
myObject.Name = "new name";
var newInner = new MyInner();
newInner.SomeValue = "new inner value";
myObject.Inner = newInner;
};
txtName.Dock = txtSomeValue.Dock = btnInit.Dock = DockStyle.Top;
form.Controls.AddRange(new Control[] { btnInit, txtSomeValue, txtName });
Application.Run(form);
}
}
}
Related
I have trouble with the ViewModel of my MVVM pattern-code.
I have a bunch of measurements and a bunch of rules to evaluate the measurements, stored in the classes Rule and Measurement. In my main class MyClass I store my Rules and Measurements then in ObservableCollections (OC) (and connected to a DataGrid).
For all n Rules I create n CollcetionOfEvaluators in one OC and pass the respective rule and all the measurements to each single one.
In each CollectionOfEvaulators I create for the one rule and the m Measurements m Evaluators in an OC.
The Evaluators take the one Rule and the one Measurement and gives back a bool if or if not the respective Measurement passes the respective Rule.
I then have a ListView that displays for each Rule a DataGrid that shows for every Measurement if it passed the Rule.
My problem is to make the Evaluator class to fire the OnPropertyChanged method, if I change the properties of one of the measurements in MyClass. How can I pass the info basically from one child to another child's child? When I play around with the DataGrid of the Evaluators, for example click on the header to rearrange it, it works. So I guess the problem is the c# code not the xaml. So I will leave it out for once. All the bindings are Mode=TwoWay (except the bool, since it has no setter) and UpdateSourceTrigger=PropertyChanged.
I tried to sketch the problem:
This is my code:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Text;
namespace demo
{
public class MyClass : INotifyPropertyChanged
{
public class Measurement : INotifyPropertyChanged
{
private double? myValue1;
public double? MyValue1
{
get { return myValue1; }
set
{
myValue1 = value;
OnPropertyChanged("MyValue1");
}
}
private double? myValue2;
public double? MyValue2
{
get { return myValue2; }
set
{
myValue2 = value;
OnPropertyChanged("MyValue2");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class EvaluationRule
{
public EvaluationRule(double Value1Min, double Value2Min)
{
this.Value1Min = Value1Min;
this.Value2Min = Value2Min;
}
public double Value1Min;
public double Value2Min;
}
public class Evaluator : INotifyPropertyChanged
{
public Evaluator(Measurement Measurement, EvaluationRule Rule)
{
this.Rule = Rule;
this.Measurement = Measurement;
}
public EvaluationRule Rule;
private Measurement measurement;
public Measurement Measurement
{
get { return measurement; }
set
{
measurement = value;
OnPropertyChanged("Measurement");
}
}
public bool IsApproved
{
get
{
if (measurement.MyValue1 > Rule.Value1Min
&& measurement.MyValue2 > Rule.Value2Min)
{
return true;
}
return false;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class CollectionOfEvaluators : INotifyPropertyChanged
{
public CollectionOfEvaluators(EvaluationRule Rule, ObservableCollection<Measurement> Measurements)
{
this.Rule = Rule;
this.Measurements = Measurements;
var Evaluators = new ObservableCollection<Evaluator>();
foreach (var _measurement in Measurements)
{
var _evaluator = new Evaluator(_measurement, this.Rule);
Evaluators.Add(_evaluator);
}
}
public EvaluationRule Rule;
private ObservableCollection<Measurement> measurements;
public ObservableCollection<Measurement> Measurements
{
get { return measurements; }
set
{
measurements = value;
OnPropertyChanged("Measurements");
}
}
private ObservableCollection<Evaluator> evaluators;
public ObservableCollection<Evaluator> Evaluators
{
get { return evaluators; }
set
{
evaluators = value;
OnPropertyChanged("Evaluators");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
private ObservableCollection<Measurement> measurements;
public ObservableCollection<Measurement> Measurements
{
get { return measurements; }
set
{
measurements = value;
OnPropertyChanged("Measurements");
}
}
private ObservableCollection<EvaluationRule> rules;
public ObservableCollection<EvaluationRule> Rules
{
get { return rules; }
set
{
rules = value;
GetCollection();
}
}
private ObservableCollection<CollectionOfEvaluators> collection;
public ObservableCollection<CollectionOfEvaluators> Collection
{
get { return collection; }
set
{
collection = value;
OnPropertyChanged("Collection");
}
}
public void GetCollection()
{
var Collection = new ObservableCollection<CollectionOfEvaluators>();
foreach (var _rule in rules)
{
var _collection = new CollectionOfEvaluators(_rule, Measurements);
Collection.Add(_collection);
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
You must delegate the event. Evaluator should listen to the PropertyChanged event of its aggregated Measurement. The handler of this event can then raise the Evaluator.PropertyChanged event in response:
public class Evaluator : INotifyPropertyChanged
{
public Evaluator(Measurement measurement, EvaluationRule rule)
{
this.Rule = rule;
this.Measurement = measurement;
this.Measurement.PropertyChanged += OnMeasurementPropertyChanged;
}
public void OnMeasurementPropertyChanged(object sender, PropertyChangedEventAgrs e)
{
OnPropertyChanged(nameof(this.Measurement));
}
private Measurement measurement;
public Measurement Measurement
{
get => this.measurement
set
{
this.measurement = value;
OnPropertyChanged(nameof(this.Measurement));
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Note that you got a spelling error when naming your class. It's Measurement - you missed an 'a'. Also parameter names should always be lowercase.
I am trying to make a really simple app to learn DataBinding and events. The following code is supposed to change the label content when i click on a button, but actually it changes the property but doesn't update the label.
This is the main code :
public MainWindow()
{
InitializeComponent();
environments = new ObservableCollection<Env>();
environments.Add(new Env("env1", new ObservableCollection<Cell>()));
environments.Add(new Env("env2", new ObservableCollection<Cell>()));
foreach (Env e in environments)
{
Label label = new Label
{
Content = e.Name
};
pnlMain.Children.Add(label);
}
}
private void ChangeEnvName_Click(object sender, RoutedEventArgs e)
{
foreach (Env env in environments)
{
env.Name = "test";
}
}
And this is the Env class :
class Env : INotifyPropertyChanged
{
//membres
#region membres
private string _name;
private ObservableCollection<Cell> _cells;
#endregion
//propriétés
#region propriétés
public string Name
{
get { return this._name; }
set
{
if (this._name != value)
{
this._name = value;
this.NotifyPropertyChanged("Name");
}
}
}
public ObservableCollection<Cell> Cells
{
get { return this._cells; }
set
{
if (this._cells != value)
{
this._cells = value;
this.NotifyPropertyChanged("Cells");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
#endregion
//méthodes
#region méthodes
public void NotifyPropertyChanged(string propName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
#endregion
//constructeur
#region contructeur
public Env(string name, ObservableCollection<Cell> cells)
{
_name = name;
_cells = cells;
}
#endregion
}
What's the problem? Isn't it suppose the update the label.content when i update Env.Name ?
You haven't bound the Content property of the Label to the Name property. You have just set it to a string. Try this:
foreach (Env e in environments)
{
Label label = new Label();
label.SetBinding(Label.ContentProperty, new Binding("Name") { Source = e });
pnlMain.Children.Add(label);
}
Or create an Environments property that returns environments, set the DataContext to this and bind to Environments[index].Name. If you don specify an explicit Source of the binding, it will look for the property in its current DataContext which may be inherited from a parent element. Please see the docs for more information.
This works (the graph is properly loaded):
var oxyPlotView = new OxyPlotView{ Model = GetPlotModelSynch() };
This doesn't (the graph remains empty):
var oxyPlotView = new OxyPlotView();
// Here PlotModel will be loaded asynchronously from the BindingContext:
oxyPlotView.SetBinding(OxyPlotView.ModelProperty, new Binding(nameof(GraphViewModel.PlotModel)));
I have made proper isolated tests to ensure that INotifyPropertyChanged is working properly with my ViewModel. So the problem seems to be that OxyPlotView is built properly only if it has al the info from its inception (?). Is that even possible?
Here is the full ViewModel. INotifyPropertyChanged works because Title is behaving as intended (Title is binded to a Label in the same view).
class GraphViewModel : INotifyPropertyChanged
{
IGraphSeriesGroupRepository _graphSeriesGroupRepository;
private GraphSeriesGroup _graphSeriesGroup;
private ulong _sensorId;
public event PropertyChangedEventHandler PropertyChanged;
private PlotModel _plotModel;
public PlotModel PlotModel
{
get { return _plotModel; }
set
{
if (_plotModel != value)
{
_plotModel = value;
OnPropertyChanged(nameof(PlotModel));
}
}
}
private string _title;
public string Title
{
get { return _title; }
set
{
if (_title != value)
{
_title = value;
OnPropertyChanged(nameof(Title));
}
}
}
private bool _isLoading;
public bool IsLoading
{
get { return _isLoading; }
set
{
_isLoading = value;
OnPropertyChanged(nameof(IsLoading));
}
}
public GraphViewModel(IGraphSeriesGroupRepository graphSeriesGroupRepository, ulong sensorId)
{
_graphSeriesGroupRepository = graphSeriesGroupRepository;
_sensorId = sensorId;
Load();
}
public PlotModel GetPlotModelSynch()
{
_graphSeriesGroup = _graphSeriesGroupRepository.GetGraphSeriesGroup(_sensorId);
return GetPlotModel(_graphSeriesGroup);
}
private async void Load()
{
IsLoading = true;
await Task.Delay(5000);
_graphSeriesGroup = await _graphSeriesGroupRepository.GetGraphSeriesGroupAsync(_sensorId);
ApplyChanges();
IsLoading = false;
}
private void ApplyChanges()
{
// ---
Title = _graphSeriesGroup.Title;
PlotModel = GetPlotModel(_graphSeriesGroup);
}
private PlotModel GetPlotModel(GraphSeriesGroup graphSeriesGroup)
{
...
}
public void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Update: The only way I've found to make it work is:
private void chatter_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if(e.PropertyName == nameof(GraphViewModel.PlotModel))
{
_oxyPlotView = new OxyPlotView
{
Model = _graphViewModel.PlotModel
};
_stackLayout.Children.Add(_oxyPlotView);
}
}
...even updating an _oxyPlotView (which was already added to the StackLayout) and calling _oxyPlotView.InvalidateDisplay() didn't work.
I have a custom class inheriting from ObservableCollection and INotifyPropertyChanged (i.e. the custom class also has properties) that serves as a Collection<T> where T also inherits from INotifyPropertyChanged:
public class CustomCollection<T> : ObservableCollection<T>, INotifyPropertyChanged where T: INotifyPropertyChanged {
private string _name;
public string Name {
get {
return _name;
}
set {
if (_name != value) {
_name = value;
NotifyPropertyChanged("Name");
}
}
}
private int _total;
public int Total {
get {
return _total;
}
set {
if (_total != value) {
_total = value;
NotifyPropertyChanged("Total");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName) {
PropertyChangedEventHandler handler = PropertyChanged;
if (null != handler) {
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
And T item class:
public class DataItem : INotifyPropertyChanged {
private string _fname;
public string Fname {
get {
return _fname;
}
set {
if (value != _fname) {
_fname = value;
NotifyPropertyChanged("Fname");
}
}
}
private int_value;
public int Value {
get {
return _value;
}
set {
if (value != _value) {
_value = value;
NotifyPropertyChanged("Value");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName) {
PropertyChangedEventHandler handler = PropertyChanged;
if (null != handler) {
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
And the ViewModel:
public class ViewModel : ViewModelBase {
private readonly IService _dataService;
private bool _isLoading;
public bool IsLoading {
get {
return _isLoading;
}
private set {
_isLoading = value;
RaisePropertyChanged("IsLoading");
}
}
private CustomCollection<DataItem> _items;
public CustomCollection<DataItem> Items
{
get
{
return _items;
}
set
{
_items= value;
RaisePropertyChanged("Items");
}
}
public ViewModel(IService dataService) {
_dataService = dataService;
}
public void Refresh() {
if (!this.IsLoading) {
this.IsLoading = true;
_dataService.RefreshData(
this, (error) => {
if (error != null) {
return;
}
if (!IsInDesignMode)
this.IsLoading = false;
}
);
}
}
public void GetData() {
if (Games == null) {
Games = new CustomCollection<DataItem>();
} else {
Games.Clear();
}
if (!this.IsLoading) {
this.IsLoading = true;
_dataService.GetData(
this, (error) => {
if (error != null) {
return;
}
if (!IsInDesignMode)
this.IsLoading = false;
}
);
}
}
And I have bound the CustomCollection<T> to a control in my View (xaml). Everything works fine initially, upon navigating to the page, the ViewModel calls for a DataService to retrieve the data and populate the CustomCollection<T>. However, when refreshing the data, the View is not updated until all the data has been iterated over and refreshed/updated!
Here is the code for the refresh/updated (keep in mind, I'm retrieving the data via a web service, and for the purposes of testing have just manually updated the Value property in DataItem at each passover of the CustomCollection<T>):
public async RefreshData(ViewModel model, Action<Exception> callback) {
if (model.Items == null) return;
// ... retrieve data from web service here (omitted) ...
foreach (DataItem item in retrievedItems) { // loop for each item in retrieved items
DataItem newItem = new DataItem() { Fname = item.Fname, Value = item.Value };
if (model.Items.contains(newItem)) { // override for .Equals in CustomCollection<T> allows for comparison by just Fname property
model.Items[model.Items.IndexOf(newItem)].Value += 10; // manual update
} else {
model.Items.Add(newItem);
}
System.Threading.Thread.Sleep(1000); // 1 second pause to "see" each item updated sequentially...
}
callback(null);
}
So in summary, how can I make it so updating Value of my DataItem will instantly reflect in the View, given my current setup of CustomCollection<DateItem>? Something to do with async perhaps? I mean, when Sleep(1000) gets called, the UI does not hang, maybe this has something to do with it?
Any ideas on how to fix this? As you might have guessed, this issue is also present when first retrieving the data (but is barely noticeable as data is retrieved/processed during the navigation to the View).
Note: I'm using the MVVMLight Toolkit.
Thanks.
I am trying to set the name (textbox) value using WCF Service. I am hosting service in WPF application. I used the MVVM Model initially to set textbox value from the MainWindow.cs and it worked. But then I made some properties static in order to access the same through the service contract. It still seems to setting the property of Model attribute but not changing value in the text box. Can anyone please guide me?
Model.cs
public class Model : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetField<T>(ref T field, T value, string propertyName)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
MessageBox.Show(field.ToString());
return true;
}
// props
private static string testname;
public static string TestName
{
get { return testname; }
set {
Model m = new Model();
m.SetField(ref testname, value, "TestName");
}
}
}
WCF InameService.cs
public class nameService : InameService
{
public void setMyName(string name)
{
Model.TestName = name;
}
}
MainWindow.xaml
<Grid Name="GridName">
<TextBox Name="TextName" HorizontalAlignment="Left" Height="23" Margin="193,140,0,0" TextWrapping="Wrap" Text="{Binding TestName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top" Width="120" />
</Grid>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
ServiceHost host = new ServiceHost(typeof(nameService));
InitializeComponent();
host.Open();
Model s = new Model();
//this.DataContext = s.NameValue.TestName;
Model.TestName = "Alicia";
this.TextName.DataContext = s;
}
}
Thanks Nathan for help. Following is the answer:
I changed the ViewModel to Singleton Class and also instantiated the composite Model object while creating the instance.
`class ViewModel
{
private static volatile ViewModel instance;
private static object _mutex = new object();
private ViewModel() { }
private Model model;
public Model NameValue
{
get { return model; }
set { model = value; }
}
public static ViewModel Instance
{
get
{
if (instance == null)
{
lock (_mutex)
{
if (instance == null)
{
instance = new ViewModel();
instance.model = new Model();
}
}
}
return instance;
}
}
}`
then changed the MainWindow.xaml.cs
try
{
ViewModel s = ViewModel.Instance;
s.NameValue.TestName = "Alicia";
this.DataContext = s;
this.TextName.DataContext = s;
}
catch (Exception e)
{
MessageBox.Show("Error" + e.Message);
}
Similar changes was done in the Service Contract Class. I hope this will help some one trying to get the value in
Don't use static properties as you can't bind to them. Use a static object instead or pass the Model object to the service for example in the constructor and use that instance for updates.
public class nameService : InameService
{
private Model model;
public nameService(Model m)
{
model = m;
}
public void setMyName(string name)
{
model.TestName = name;
}
}
public class Model : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetField<T>(ref T field, T value, string propertyName)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
MessageBox.Show(field.ToString());
return true;
}
// props
private string testname;
public string TestName
{
get { return testname; }
set {
Model m = new Model();
m.SetField(ref testname, value, "TestName");
}
}
}