Binding control to Object vs DataRow - c#

I wrote some example code to demonstrate my question. one is bound to Object and other to DataRow:
Bind to DataRow example:
namespace WindowsFormsApplication1
{
public partial class frmBindExample : Form
{
public frmBindExample()
{
InitializeComponent();
InitForm();
}
private void InitForm()
{
//;; Init the list
DataTable dt = new DataTable();
dt.Columns.Add(new DataColumn("Id"));
dt.Columns.Add(new DataColumn("Name"));
dt.Rows.Add(new string[] { "5476", "Smith" });
dt.Rows.Add(new string[] { "5477", "Marlin" });
Label label1 = new Label() { Top = 130, Left = 10, Text = "Id of Smith is:" };
this.Controls.Add(label1);
//;; Bind two direction with TextBox.
TextBox textbox1 = new TextBox() { Top = 130, Left = 130, Width = 100 };
this.Controls.Add(textbox1);
textbox1.DataBindings.Add("Text", dt.Rows[0], "Id");
//;; The binding system respose changing property value
Button button1 = new Button() { Top = 160, Left = 10, Width = 200, Text = "Set Id=99 Directly by property" };
this.Controls.Add(button1);
button1.Click += (s, e) =>
{
dt.Rows[0]["Id"] = "99";
};
DataGridView dg = new DataGridView() { Top = 200, Left = 10 };
this.Controls.Add(dg);
dg.DataSource = dt;
}
}
}
It's look like:
As you can see, the binding to TextBox not work as it on next example. But, when I update the field by pressing on the button, the data grid refresh immediately:
Ok, now take look what hempen if I bind Object instead:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsApplication1
{
public partial class frmBindExample : Form
{
public frmBindExample()
{
InitializeComponent();
InitForm();
}
private void InitForm()
{
//;; Init the list
List<Person> lst = new List<Person>();
lst.Add(new Person() { Id = "5476", Name = "Smith" });
lst.Add(new Person() { Id = "5477", Name = "Marlin" });
Label label1 = new Label() { Top = 130, Left = 10, Text = "Id of Smith is:" };
this.Controls.Add(label1);
//;; Bind two direction with TextBox.
TextBox textbox1 = new TextBox() { Top = 130, Left = 130, Width = 100 };
this.Controls.Add(textbox1);
textbox1.DataBindings.Add("Text", lst[0], "Id");
//;; The binding system respose changing property value
Button button1 = new Button() { Top = 160, Left = 10, Width = 200, Text = "Set Id=99 Directly by property" };
this.Controls.Add(button1);
button1.Click += (s, e) =>
{
lst[0].Id = "99";
};
DataGridView dg = new DataGridView() { Top = 200, Left = 10 };
this.Controls.Add(dg);
dg.DataSource = lst;
}
}
//;; The person class can bind any his property without any extra call fo change detection
public class Person
{
public string Id { get; set;}
public string Name { get; set; }
}
}
Now, the TextBox show the Id value as we aspect. But push on the Set button not refresh data on the DataGrid.
So, my question is:
Why binding TextBox not work correctly on the first example?
It's true to say, that automatic (without any extra call for do binding) propagate update from source to control happens only with DataRow?

Why binding TextBox not work correctly on the first example?
It's because the TypeDescriptor of the DataRow doesn't have a Id property. Consider the following rules:
When data binding to a property of an item, the type descriptor of the item should contain a property with that name.
When data binding to a list, the list item type descriptor should contain a property with that name.
Is it true to say, that automatic (without any extra call for do binding) propagate update from source to control happens only with DataRow?
No. It's not because of DataRow type. It's because of INotifyPropertyChanged and IBindingList. Consider the following rules:
When data binding a control to an item, if the item implements INotifyPropertyChanged, then UI will be updated immediately after updating item.
When data-binding to a list control, if the item Implements INotifyPropertyChanged and the list implement IBindingList, the UI will be updated immediately after updating item or list.
More information
What I described above in short, you can find in details in Windows Forms Data Binding. I recommend reading the following useful documents:
Data Sources Supported by Windows Forms
Interfaces Related to Data Binding
Change Notification in Windows Forms Data Binding

Since, maybe, describing this procedure in the comments in not a great idea, here's the enlarged version.
Define a BindingList or a List<T> as the data storage object (I prefer a BindingList, but a simple List, here, will do the job anyway).
Define a BindingSource that will provide change notifications and currency management. It greatly simplifies the binding of controls in WinForms (the class object should implement INotifyProeprtyChanged, but for the purpose of this example is not important. It may become important in more specific scenarios, when you have two-way bindings that need to update the UI and the source of data immediately).
Set the BindingSource.DataSource property to the object that provides the data: here, a BindingList or an IList.
Add a Binding to the TextBox.Text property which will be bound to a property of the Data source (or a Column of a DataTable, for example), setting the DataSource value of the Binding to the BindingSource and the DataMember value to the Property (or Column) of the data source to which the TextBox property is bound. Here, the Id property of the Input class.
Subscribe to the Parse event of the TextBox Binding, to provide means to validate the data entered in the TextBox before allowing an update of the Data source. If the value entered doesn't fit the description (i.e., a user entered letters instead of numbers), we can call, for example, the BindingSource.ResetCurrentItem method to cancel the data update
Set the DataGridView.DataSource to the BindingSource.
This is what happens, using the code shown here:
Note:
I'm using a lambda here to subscribe to the Parse event; you may want to use a separate handler if you need to subscribe/unsubcribe to this event more than once.
internal class Input
{
public int Id { get; set; }
public string Name { get; set; }
}
internal List<Input> inputData = new List<Input>();
internal BindingSource bindingSource;
private void button1_Click(object sender, EventArgs e)
{
bindingSource = new BindingSource();
inputData.AddRange(new [] {
new Input() { Id = 5476, Name = "Smith" },
new Input() { Id = 5477, Name = "Marlin" }
});
bindingSource.DataSource = inputData;
Binding tboxBind = new Binding("Text", bindingSource, "Id", false, DataSourceUpdateMode.OnPropertyChanged);
tboxBind.Parse += (pObj, pEvt) =>
{
if (!int.TryParse(pEvt.Value.ToString(), out int value))
bindingSource.ResetCurrentItem();
};
textBox1.DataBindings.Add(tboxBind);
dataGridView1.DataSource = bindingSource;
}

Related

Updating a Label with DataBinding

After my balance label is initially bound to a number, changing the datasource again doesn't update the value again.
I want to update the a Windows Form Label automatically after the database object is changed and I re-pull it into the constructorData.BankAccount.
public class ConstructorData
{
public Client Client { get; set; }
public BankAccount BankAccount { get; set; }
}
private void frmTransaction_Load(object sender, EventArgs e)
{
// Pretend we populated constructor data already
// This line of code is working
bankAccountBindingSource.DataSource = constructorData.BankAccount;
}
private void lnkProcess_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
{
constructorData.BankAccount = db.BankAccounts.Where(x => x.BankAccountId == constructorData.BankAccount.BankAccountId).SingleOrDefault();
// What do I do here
// Doesn't work
bankAccountBindingSource.EndEdit();
bankAccountBindingSource.ResetBindings(false);
}
Auto generated code:
//
// lblAccountBalance
//
this.lblAccountBalance.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.lblAccountBalance.DataBindings.Add(new System.Windows.Forms.Binding("Text", this.bankAccountBindingSource, "Balance", true));
this.lblAccountBalance.Location = new System.Drawing.Point(482, 71);
this.lblAccountBalance.Name = "lblAccountBalance";
this.lblAccountBalance.Size = new System.Drawing.Size(196, 23);
this.lblAccountBalance.TabIndex = 7;
this.lblAccountBalance.Text = "label1";
Since here (inside the form load):
bankAccountBindingSource.DataSource = constructorData.BankAccount;
you bind directly to the BankAccount instance, even implementing INotifyPropertyChanged in ConstructorData class (as suggested in the comments) will not help.
With that design, anytime you assign a new BankAccount instance to the ConstructorData.BankAccount property (as in the shown code), you need also to set it as DataSource of the BindingSource used:
constructorData.BankAccount = db.BankAccounts.Where(x => x.BankAccountId == constructorData.BankAccount.BankAccountId).SingleOrDefault();
// What do I do here
bankAccountBindingSource.DataSource = constructorData.BankAccount;
Without Implementing INotifyPropertyChanged Ivan's answer is exactly what you need.
The reason is because you put an object in DataSource of binding source this way: BindingSource.DataSource = constructorData.BankAccount, so it uses the object which is in BankAccount property as data source. If you change the value of constructorData.BankAccount, you disd't changed the data source of BindingSource and it will contain the previous object. For example take a look at this code:
var a = new MyClass("1"); // ← constructorData.BankAccount = something;
var b = a; // ← bindingSource.DataSource = constructorData.BankAccount.
a = new MyClass("2"); // ← constructorData.BankAccount = something else;
What should contain b now? Do you expect b contains MyClass("1")? Surely no.
For more information take a look at this post:
Data Binding doesn't work when I assign a new object instance to the bound variable
Can I use INotifyPropertyChanged to solve the problem?
If you implement INotifyPropertyChanged in ConstructorData and change bindings this way, yes:
bankAccountBindingSource.DataSource = constructorData;
//...
this.lblAccountBalance.DataBindings.Add(new System.Windows.Forms.Binding("Text",
this.bankAccountBindingSource, "BankAccount.Balance", true));

Changing Xamarin Label in ListCell does not work

I have a problem with a ListView. I want each Cell to have a label and a switch but the text of the label does not appear.
Here is my code:
public class FilterPage : ContentPage
{
public FilterPage()
{
List<FilterCell> listContent = new List<FilterCell>();
foreach(string type in Database.RestaurantTypes)
{
FilterCell fc = new FilterCell();
fc.Text = type;
listContent.Add(fc);
}
ListView types = new ListView();
types.ItemTemplate = new DataTemplate(typeof(FilterCell));
types.ItemsSource = listContent;
var layout = new StackLayout();
layout.Children.Add(types);
Content = layout;
}
}
public class FilterCell : ViewCell
{
private Label label;
public Switch CellSwitch { get; private set; }
public String Text{ get { return label.Text; } set { label.Text = value; } }
public FilterCell()
{
label = new Label();
CellSwitch = new Switch();
var layout = new StackLayout
{
Padding = new Thickness(20, 0, 0, 0),
Orientation = StackOrientation.Horizontal,
HorizontalOptions = LayoutOptions.FillAndExpand,
Children = { label, CellSwitch }
};
View = layout;
}
}
If I enter a fixed Text in the FilterCell-Constructor it works fine (e.g.: label.Text = "Hello World")
When I create a Method for the ItemSelected-Event and read out the SelectedItem.Text Property I get the text I assigned as Value but it's never displayed. Only the switch is displayed when I try to run this Code.
Thanks for your help
Niko
Ohh boy. This code looks like a rape (sorry I had to say this).
Now let's see what's wrong:
The reason is you are mixing up data and view heavily.
The line
types.ItemTemplate = new DataTemplate(typeof(FilterCell));
means: "For each item in the list (ItemsSource) create a new filter cell". The FilterCells that you create in the loop are never displayed.
The easy fix
public class FilterPage : ContentPage
{
public FilterPage()
{
var restaurantTypes = new[] {"Pizza", "China", "German"}; // Database.RestaurantTypes
ListView types = new ListView();
types.ItemTemplate = new DataTemplate(() =>
{
var cell = new SwitchCell();
cell.SetBinding(SwitchCell.TextProperty, ".");
return cell;
});
types.ItemsSource = restaurantTypes;
Content = types;
}
}
There is a standard cell type that contains a label and a switch SwitchCell, use it.
As ItemsSource of your list, you have to use your data. In your case the list of restaurant types. I just mocked them with a static list.
The DataTemplate creates the SwitchCell and sets the Databinding for the Text property. This is the magic glue between View and data. The "." binds it to the data item itself. We use it, because our list contains items of strings and the Text should be exactly the string. (read about Databinding: https://developer.xamarin.com/guides/xamarin-forms/getting-started/introduction-to-xamarin-forms/#Data_Binding )
I striped away the StackLayout that contained the list. You can directly set the list as Content of the page.
Lesson
use standard controls, if possible
You should always try to remember to keep data and view apart from each other and use data binding to connect to each other.
Try to avoid unnecessary views.

Setting controls in a Telerik GridView cell

Here's a concrete example of what I am attempting to do with the Telerik GridView control. Let's say I have an application that will read a delimited file (say CSV) which will have n columns. The value n can vary from file-to-file but is constant within a file. The application starts with an empty grid and adds columns as required to match the input file. The grid displays all data as strings in all cells. This works with either binding to a BindingList or putting the record (objects) into the GridView.Items list.
Now, what I want to do is put a single row at the top of the grid (a row that will not scroll) that contains comboboxes. That is, at the top of each column, the first row contains a combobox. On the first pass, the combobox will only be a drop list, but on the next pass I will add another row with a set of comboboxes that will be editable. For now, let's only consider drop lists.
The specific problem that I have is that I do not see how to set a specific type of control for a particular cell. Telerik provides a GridViewComboBoxColumn class that will define the behavior for an entire column but that's not what I need.
Because of the variable number of columns, I think that the code-behind would be the place to work this magic. I may have to do something in the xaml but, since I've only been in WPF for a few months, nothing is jumping out at me.
I've done something like this with the DataGridView and XtraGrid, but this one has me stumped. Pointers would be much appreciated.
In response to Jonathan D's answer, I have taken the provided code and modified it to recognize when a cell on the 0th row is being edited. When this is the case, a drop list is presented when the user initiates an edit operation.
using Telerik.Windows.Controls;
using Telerik.Windows.Controls.GridView;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace LLamasoft.DataGuru.Plugin.Internal.ConfigurationUI
{
public class RadGridViewComboboxHeaderColumn : GridViewBoundColumnBase
{
public static readonly DependencyProperty SelectedStringProperty =
DependencyProperty.Register("SelectedString", typeof(string), typeof(RadGridViewComboboxHeaderColumn), new PropertyMetadata(null));
public string SelectedString
{
get { return (string) GetValue(SelectedStringProperty); }
set { SetValue(SelectedStringProperty, value); }
}
public override FrameworkElement CreateCellEditElement(GridViewCell cell, object dataItem)
{
// we need the row on which this cell lives
GridViewDataControl gridViewDataControl = cell.ParentRow.GridViewDataControl;
object currentEditItem = gridViewDataControl.Items.CurrentEditItem;
int index = gridViewDataControl.Items.IndexOf(currentEditItem);
FrameworkElement frameworkElement = null;
if (index == 0)
{
BindingTarget = ComboBox.SelectedValueProperty;
ComboBox comboBox = new ComboBox();
// seed some values,
// this list should be set right after construction if static, otherwise via callback if dynamic
comboBox.Items.Add(string.Empty);
comboBox.Items.Add("apples");
comboBox.Items.Add("oranges");
if (!comboBox.Items.Contains(cell.Value))
{
comboBox.Items.Add(cell.Value);
}
comboBox.SelectedValue = SelectedString;
frameworkElement = comboBox;
}
else
{
BindingTarget = TextBox.TextProperty;
TextBox textBox = new TextBox();
textBox.Text = SelectedString;
frameworkElement = textBox;
}
frameworkElement.SetBinding(BindingTarget, CreateValueBinding());
return frameworkElement;
}
public override object GetNewValueFromEditor(object editor)
{
// ensure that the control will return the correct value when queried for it
ComboBox comboBox = editor as ComboBox;
if (comboBox != null)
{
// bound to comboBox.SelectedValue which carries the correct value
}
TextBox textBox = editor as TextBox;
if (textBox != null)
{
// bound to textBox.Text which carries the correct value
}
return base.GetNewValueFromEditor(editor);
}
private Binding CreateValueBinding()
{
Binding valueBinding = new Binding();
valueBinding.Mode = BindingMode.TwoWay;
valueBinding.NotifyOnValidationError = true;
valueBinding.ValidatesOnExceptions = true;
valueBinding.UpdateSourceTrigger = UpdateSourceTrigger.Explicit;
valueBinding.Path = new PropertyPath(this.DataMemberBinding.Path.Path);
return valueBinding;
}
}
}
The good news is that this shows that any edit control can be used in any cell based on requirements.
The bad parts are: 1) a dummy record has to be inserted at the 0th list position and must be maintained there, 2) the data is being stored back into the field on the 0th record and may require a different data type than is on the equivalent fields on the other records, and 3) the combobox is only shown when the cell is in the edit mode.
The latter issue for me may not be an issue elsewhere. I want a visual cue that the user is expected to interact with the cells at the top of the columns. Using this method, there is no differentiating factor between the top row and the rest of the rows until the edit operation begins. My ideal solution would have the cells always show their comboboxes.
One other issue that I find difficult to believe that I am facing is the fact that I cannot easily pin/freeze topmost rows. I want this line to always remain at the top after scrolling. There is no _grid.Rows[0].IsPinned = true functionality.
Telerik has responded to my request for info and suggests that I use a template selector to determine how the cell is represented. (http://www.telerik.com/community/forums/wpf/gridview/need-just-first-row-in-grid-to-be-all-comboboxes.aspx#1820310). At this point, I turn my attention to testing that method.
You want to create your own column
using System.Windows;
using System.Windows.Data;
using Telerik.Windows.Controls;
using Telerik.Windows.Controls.GridView;
using System;
namespace Inspect
{
public class DateTimePickerColumn : GridViewBoundColumnBase
{
public TimeSpan TimeInterval
{
get
{
return (TimeSpan) GetValue(TimeIntervalProperty);
}
set
{
SetValue(TimeIntervalProperty, value);
}
}
public static readonly DependencyProperty TimeIntervalProperty =
DependencyProperty.Register("TimeInterval", typeof(TimeSpan), typeof(DateTimePickerColumn), new PropertyMetadata(TimeSpan.FromHours(1d)));
public override FrameworkElement CreateCellEditElement(GridViewCell cell, object dataItem)
{
this.BindingTarget = RadDateTimePicker.SelectedValueProperty;
RadDateTimePicker picker = new RadDateTimePicker();
picker.IsTooltipEnabled = false;
picker.TimeInterval = this.TimeInterval;
picker.SetBinding(this.BindingTarget, this.CreateValueBinding());
return picker;
}
public override object GetNewValueFromEditor(object editor)
{
RadDateTimePicker picker = editor as RadDateTimePicker;
if (picker != null)
{
picker.DateTimeText = picker.CurrentDateTimeText;
}
return base.GetNewValueFromEditor(editor);
}
private Binding CreateValueBinding()
{
Binding valueBinding = new Binding();
valueBinding.Mode = BindingMode.TwoWay;
valueBinding.NotifyOnValidationError = true;
valueBinding.ValidatesOnExceptions = true;
valueBinding.UpdateSourceTrigger = UpdateSourceTrigger.Explicit;
valueBinding.Path = new PropertyPath(this.DataMemberBinding.Path.Path);
return valueBinding;
}
}
}
That is how you create a custom column. If you modify the CreateCellEditElement Method it will let you create custom cells how you like. You should even be able to detect the row number

Trying to Bind List<T> to CheckedListBox in WinForms c#

I am using WinForms C#
Is there any way to get following behavior:
bind List to CheckedListBox
When I add elements to list CheckedList box refereshes
When I change CheckedListBox the list changes
I tried to do the following:
Constructor code:
checkedlistBox1.DataSource = a;
checkedlistBox1.DisplayMember = "Name";
checkedlistBox1.ValueMember = "Name";
Field:
List<Binder> a = new List<Binder> { new Binder { Name = "A" } };
On button1 click:
private void butto1_Click(object sender, EventArgs e)
{
a.Add(new Binder{Name = "B"});
checkedListBox1.Invalidate();
checkedListBox1.Update();
}
But the view does not update .
Thank You.
Change this line:
List<Binder> a = new List<Binder> { new Binder { Name = "A" } };
to this:
BindingList<Binder> a = new BindingList<Binder> { new Binder { Name = "A" } };
It will just work without any other changes.
The key is that BindingList<T> implements IBindingList, which will notify the control when the list changes. This allows the CheckedListBox control to update its state. This is two-way data binding.
Also, you could change these two lines:
checkedListBox1.Invalidate();
checkedListBox1.Update();
to this (more readable and essentially does the same thing):
checkedListBox1.Refresh();
Two things you may wish to look at:
Use a BindingList
Add a BindableAttribute to your Name property
Does your List<Bender> need to be some kind of observable collection, like ObservableCollection<Bender> instead?
The proper way of binding a checked listbox is:
List<YourType> data = new List<YourType>();
checkedListBox1.DataSource = new BindingList<YourType>(data);
checkedListBox1.DisplayMember = nameof(YourType.Name);
checkedListBox1.ValueMember = nameof(YourType.ID);
Note to self.
The issue i have every time binding it is that the properties DataSource, DisplayMember and ValueMember are not suggested by intellisense and i get confused.

How to databind a Linq2SQL collection to winform textfields

I am a little bit puzzled as to how I can optimize my program by utlizing DataBindings. My program uses several Linq2SQL bound Objects storing the Data. All ORM objects are stored in a hierarchy. In a second GUI project I am displaying this data in some Text and Combo Box fields.
The data structure hierarchy is as follows:
JobManager contains a Dictionary of Jobs
Each Job contains a Dictionary of Jobitems
Each Jobitem contains exactly one Article
Job, Jobitem and Article each are Linq2SQL objects, representing a ORM.
Now I have a GUI with 2 list views and a tab pane. The tab pane displays the properties of jobs, jobitems and articles and offers the possibility to modify jobs and jobitems. The GUI should behave like this:
When a Job is selected in the first ListView, the related jobitems will be shown in the second ListView and detail information about the job are shown in the tab pane.
When a Jobitem is selected in the second ListView, the jobitem details and article details are shown in the tab pane, but only the Jobitem info is editable.
When changes are done, the user has to intentionally save them. Otherwise the changes should be discarded and not synced to the database.
How can I achieve this behaviour with DataBinding?
Especially, can I bind a complete collection to a single TextField once and shift through its position dictated by the selection in the ListViews? Or do I have to add and remove individual DataBindings on a per Job basis for every selection the user conducts?
Do you really mean "Dictionary"? Winform binding is OK with lists (IList/IListSource), but not with dictionary. Additionally, ListView isn't quite as easy to bind to as some other controls.
Other than that, it should work purely using the mapping names - I'll try to do a simple example...
Edit with basic example from Northwind; note that the data-context should ideally not be long lived; you may also want to look at things like repository implementations rather than direct binding:
using System;
using System.Windows.Forms;
using SomeNamespaceWithMyDataContext;
static class Program
{
[STAThread]
static void Main() {
MyDataContext ctx = new MyDataContext();
BindingSource custs = new BindingSource() {
DataSource = ctx.Customers};
BindingSource orders = new BindingSource {
DataMember = "Orders", DataSource = custs};
Button btn;
using (Form form = new Form
{
Controls = {
new DataGridView() {
DataSource = orders, DataMember = "Order_Details",
Dock = DockStyle.Fill},
new ComboBox() {
DataSource = orders, DisplayMember = "OrderID",
Dock = DockStyle.Top},
new ComboBox() {
DataSource = custs, DisplayMember = "CompanyName",
Dock = DockStyle.Top},
(btn = new Button() {
Text = "Save", Dock = DockStyle.Bottom
}), // **edit here re textbox etc**
new TextBox {
DataBindings = {{"Text", orders, "ShipAddress"}},
Dock = DockStyle.Bottom
},
new Label {
DataBindings = {{"Text", custs, "ContactName"}},
Dock = DockStyle.Top
},
new Label {
DataBindings = {{"Text", orders, "RequiredDate"}},
Dock = DockStyle.Bottom
}
}
})
{
btn.Click += delegate {
form.Text = "Saving...";
ctx.SubmitChanges();
form.Text = "Saved";
};
Application.Run(form);
}
}
}
As an aside - note that the syntax:
DataBindings = {{"Text", orders, "ShipAddress"}}
Is equivalent to:
someTextBox.DataBindings.Add("Text", orders, "ShipAddress");
(I only add this as it is a common question)

Categories