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.
Related
I know this is terribly common issue, but I just can't get the button to update to "Pressed1" and "Pressed2" content when changing "Default" of buttonContent. Having looked at few questions, I can't find the answer that'd work for me, I simply can't find out what is wrong here, so here's the crappy code:
The window with a button
public partial class MainWindow : Window
{
Code_Behind cB;
public MainWindow()
{
cB = new Code_Behind();
this.DataContext = cB;
InitializeComponent();
}
private void button_Click(object sender, RoutedEventArgs e)
{
cB.buttonPressed();
}
}
And here's the separate class
public class Code_Behind : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _buttonContent = "Default";
public string buttonContent
{
get { return _buttonContent; }
set {
if (_buttonContent != value)
{
buttonContent = value;
OnPropertyChanged("buttonContent");
}
}
}
public void buttonPressed()
{
int timesPressed = 0;
if (timesPressed != 1)
{
_buttonContent = "Pressed1";
timesPressed++;
}
else if (timesPressed != 2)
{
_buttonContent = "Pressed2";
timesPressed++;
timesPressed = 0;
}
}
protected void OnPropertyChanged(string name)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
You are not setting the property, but the backing field. Hence the PropertyChanged event is not fired.
Replace
_buttonContent = "Pressed1";
...
_buttonContent = "Pressed2";
with
buttonContent = "Pressed1";
...
buttonContent = "Pressed2";
Besides that, it is a widely accepted convention to write property names with Pascal casing, i.e. ButtonContent instead of buttonContent.
Moreover, your property setter looks odd (probably because you try to squeeze too much code in one line).
Instead of
set
{
if (_buttonContent != value)
{
_buttonContent = value;
}
OnPropertyChanged("buttonContent");
}
it should certainly be
set
{
if (_buttonContent != value)
{
_buttonContent = value;
OnPropertyChanged("buttonContent");
}
}
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.
Just getting started with data binding in C# and looking for some help. The below binding statements break (Visible property stops toggling with MyBool & MyBoolInverse) when the line binding SelectedItem of the combo box to MyEnumVar of the BusinessObject executes. Binding directly to the object instead of the BindingSource, or binding to SelectedValue instead of SelectedItem, has the same effect. Further, the value of MyEnumVar doesn't change with selections to the combo box. What am I doing wrong?
public partial class Form1 : Form
{
BindingSource bs = new BindingSource();
private BusinessObject bo = new BusinessObject();
public Form1()
{
InitializeComponent();
bs.DataSource = bo;
// Checkbox determines what type of dialog to display.
boolCheckBox.DataBindings.Add("Checked", bs, "MyBool", true,
DataSourceUpdateMode.OnPropertyChanged);
trueBox.DataBindings.Add("Visible", bs, "MyBoolInverse");
falseComboBox.DataBindings.Add("Visible", bs, "MyBool");
falseBox.DataBindings.Add("Visible", bs, "MyBool");
falseButton.DataBindings.Add("Visible", bs, "MyBool");
myEnumComboBox.DataSource = Enum.GetValues(
typeof(BusinessObject.MyEnum));
// Line below breaks above bindings, same for SelectedValue.
myEnumComboBox.DataBindings.Add("SelectedItem", bs, "MyEnumVar");
}
}
class BusinessObject : INotifyPropertyChanged
{
public enum MyEnum { RED, BLU }
MyEnum _MyEnumVar;
public MyEnum MyEnumVar
{
get { return _MyEnumVar; }
set
{
if (value != _MyEnumVar)
{
_MyEnumVar = value;
NotifyPropertyChanged("MyEnumVar");
}
}
}
private bool _MyBool;
public bool MyBool
{
get { return _MyBool; }
set
{
if (value != _MyBool)
{
_MyBool = value;
MyBoolInverse = !value;
NotifyPropertyChanged("MyBool");
}
}
}
private bool _MyBoolInverse;
public bool MyBoolInverse
{
get { return _MyBoolInverse; }
private set
{
if (value != _MyBoolInverse)
{
_MyBoolInverse = value;
NotifyPropertyChanged("MyBoolInverse");
}
}
}
public BusinessObject()
{
MyBoolInverse = !MyBool;
MyEnumVar = MyEnum.BLU;
}
// Boilerplate INotifyPropertyChanged implementation & helper.
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName = "")
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Visible property has problems with binding. Try manual 'binding'. Something along the lines of
trueBox.Visible = bo.MyBoolInverse;
bo.PropertyChanged += (s, e) => {
if(e.PropertyName == "MyBoolInverse")
trueBox.Visible = bo.MyBoolInverse;
};
Edit: Also, binding to MyEnumVar is not working beacause it is not declared as a public property.
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);
}
}
}