.NET Forum / Languages / C# / January 2008
How to DataBind a UserControl with ComboBox?
|
|
Thread rating:  |
Steve K. - 20 Jan 2008 18:19 GMT ** I just posted another, long thread related to UserControls and DataBinding, but this is separate and smaller in scope so I mad a new thread. **
OK, simple question (I hope): I have a UserControl with a ComboBox inside and I would like to be able to DataBind to the UserControl and hook up to it's ComboBox DataBinding properties.
For example: <psuedo code> class Address { // usual address fields }
class AddressService { // application wide service for address related tasks static List<string> GetStateList(); }
class AddressUserControl { // textboxes for Street, City, etc // combobox for state }
// From inside my Form that is hosting the AddressUserControl // Create a new address to bind to the UserControl Address address = new Address();
// Setup the Address User Control with the list of states (somehow?) UserControl addressUserControl = new UserControl(); addressUserControl.States[?] = AddressService.GetStateList();
// Bind my Address instance to the User Control // ? How? </psuedo code>
A quick test of dropping a combo box in a UserControl, then dropping the User Control on a Form shows that the properties of the ComboBox are hidden from the editors Formatting and Advanced Binding tools (expected)
The ComboBox seems to have the following BindableProperties: SelectedItem SelectedValue Text
Do I need to expose these properties to my UserControl's interface so that a consuming object can get to them?
What do you all do in this circumstance?
-Steve
Marc Gravell - 20 Jan 2008 19:16 GMT > Do I need to expose these properties to my UserControl's interface so that a > consuming object can get to them? Yes; making some public properties for what you need (that are simply proxies to the inner control) is the way to go. You can cheat and change the protection modifier of the control itself to "internal", but this doens't make for a maintainable system, as you can't change the internals of the control without changing everything that *uses* the control.
Marc
Steve K. - 20 Jan 2008 19:32 GMT >> Do I need to expose these properties to my UserControl's interface so >> that a [quoted text clipped - 8 lines] > > Marc I don't know how good of an imagination you have.. but try to picture this:
I've been sitting here pounding coffee trying to recover from my whopping 2.5 hours of sleep hitting F5 on Outlook Express waiting for someone to respond.
Now I'm off!! I have my direction!!
:0) (thanks)
Nicholas Paldino [.NET/C# MVP] - 20 Jan 2008 21:35 GMT Steve,
It should be noted that you aren't going to get designer support for setting the data source by just exposing the property.
In order to get designer support for the DataSource, you will have to add the AttributeProviderAttribute to the property, specifying the type of IListProvider for the constructor. This give your property designer support.
When exposing properties that are really just wrappers for properties on contained controls, you should look at the attributes that are attached to the properties in Reflector. These attributes will help with providing the designer support when the simple property declaration isn't enough.
 Signature - Nicholas Paldino [.NET/C# MVP] - mvp@spam.guard.caspershouse.com
>>> Do I need to expose these properties to my UserControl's interface so >>> that a [quoted text clipped - 21 lines] > > (thanks) Steve K. - 20 Jan 2008 22:01 GMT More Direction!! I love it! ;0)
Thanks Nicholas, I had been using [EditorBrowsable(EditorBrowsableState.Always)] to get them to show up, but taking just a *peek* via reflector should give some more good ideas, thanks for the suggestion.
> Steve, > [quoted text clipped - 36 lines] >> >> (thanks) Marc Gravell - 20 Jan 2008 22:20 GMT For info, EditorBrowsable mainly controls intellisense, not the designer. As Nicholas says - looking at the properties for similar controls is a good route - either by reflection, ILDASM, or reflector [the easiest route].
Marc
Steve K. - 21 Jan 2008 07:45 GMT Another day on this and I still can't get things working. I've taken all your advice (as best I've understood it) and created the most simple example I can think of: User Control with two TextBox controls. Here is the code for the UserControl: <code> using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Drawing.Design; using System.Data; using System.Text; using System.Windows.Forms;
namespace PMD.Library.WinFormControls { public partial class ReallySimpleAddress : UserControl { public ReallySimpleAddress() { InitializeComponent(); }
[Editor("System.ComponentModel.Design.MultilineStringEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor)), SettingsBindable(true)] [Bindable(true)] public string Address1 { get { return textBox1.Text; } set { textBox1.Text = value; } }
[Editor("System.ComponentModel.Design.MultilineStringEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor)), SettingsBindable(true)] [Bindable(true)] public string Address2 { get { return textBox2.Text; } set { textBox2.Text = value; } } } }
// Designer.cs code (in case it helps)
namespace PMD.Library.WinFormControls { partial class ReallySimpleAddress { /// <summary> /// Required designer variable. /// </summary> private System.ComponentModel.IContainer components = null;
/// <summary> /// Clean up any resources being used. /// </summary> /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param> protected override void Dispose(bool disposing) { if (disposing && (components != null)) { components.Dispose(); } base.Dispose(disposing); }
#region Component Designer generated code
/// <summary> /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// </summary> private void InitializeComponent() { this.textBox1 = new System.Windows.Forms.TextBox(); this.textBox2 = new System.Windows.Forms.TextBox(); this.SuspendLayout(); // // textBox1 // this.textBox1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top
| System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.textBox1.Location = new System.Drawing.Point(3, 3); this.textBox1.Name = "textBox1"; this.textBox1.Size = new System.Drawing.Size(100, 20); this.textBox1.TabIndex = 0; // // textBox2 // this.textBox2.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top
| System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.textBox2.Location = new System.Drawing.Point(3, 29); this.textBox2.Name = "textBox2"; this.textBox2.Size = new System.Drawing.Size(100, 20); this.textBox2.TabIndex = 1; // // ReallySimpleAddress // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.Controls.Add(this.textBox2); this.Controls.Add(this.textBox1); this.Name = "ReallySimpleAddress"; this.Size = new System.Drawing.Size(109, 54); this.ResumeLayout(false); this.PerformLayout();
}
#endregion
private System.Windows.Forms.TextBox textBox1; private System.Windows.Forms.TextBox textBox2; } } </code>
I then have a simple Windows application with a single form. I've created once instance of my "Address" object (dumb object, only fields and properties, doesn't inherit from anything (except object of course)
I've added the UserControl to my form, then from the property editor created a BindingSource from my Address object. I've bound the Address1 property of the UserControl to the Address1 property of the business object and done the same for Address2.
When I run my application and type in the Address1 field of the user control, then leave the control (lose focus) the new value sticks. Good. When I type in the Address2 field and leave focus it reverts back to the value it was initialized to! Ahh... I don't get it.
I've looked at the properties of a TextBox in reflector. The text property has no special attributes, so I don't know how it shows up in the binding area of the properties editor. I added the [Binding(true)] attribute which seems to be the attribute that makes the property appear in the bindable properties list.
Here is some more relevant code: <code> public Form1() { _address.Address1 = "350 N. Maplewood St."; _address.Address2 = "Suite 101"; _address.City = "Orange"; _address.State = "CA"; _address.Zipcode = "92866";
InitializeComponent();
// Address testing addressBindingSource.DataSource = _address; }
namespace PMD.BusinessEntities.NetsuiteEntities { public class Address { private string _address1; private string _address2; private string _city; private string _state; private string _zipcode;
public string Address1 { get { return _address1; } set { _address1 = value; } }
public string Address2 { get { return _address2; } set { _address2 = value; } }
public string City { get { return _city; } set { _city = value; } }
public string State { get { return _state; } set { _state = value; } }
public string Zipcode { get { return _zipcode; } set { _zipcode = value; } } } }
// // reallySimpleAddress1 // this.reallySimpleAddress1.Address1 = ""; this.reallySimpleAddress1.Address2 = ""; this.reallySimpleAddress1.DataBindings.Add(new System.Windows.Forms.Binding("Address1", this.addressBindingSource, "Address1", true)); this.reallySimpleAddress1.DataBindings.Add(new System.Windows.Forms.Binding("Address2", this.addressBindingSource, "Address2", true)); this.reallySimpleAddress1.Location = new System.Drawing.Point(361, 137); this.reallySimpleAddress1.Name = "reallySimpleAddress1"; this.reallySimpleAddress1.Size = new System.Drawing.Size(109, 54); this.reallySimpleAddress1.TabIndex = 5;
</code>
I REALLY hope someone sees the problem or has some idea what in the world I'm doing wrong. This is much more complex and difficult that I had expected.
If a simple and complete (as Mr. Skeet usually requests) would be in order let me know and I will zip and post online.
Thanks for any help, Steve
> For info, EditorBrowsable mainly controls intellisense, not the > designer. As Nicholas says - looking at the properties for similar > controls is a good route - either by reflection, ILDASM, or reflector > [the easiest route]. > > Marc Steve K. - 21 Jan 2008 07:59 GMT This sounds like my problem:
"There is a bug in databinding which appears when you are at the first row in the datasource (row 0) and you modify more than one property of the control. This bug causes the second modification to be lost." http://www.freeweb.hu/noiseehc/SimpleBind.html
Now I'm not dealing with "rows" per se, but the second part of the quote seems right on.
> Another day on this and I still can't get things working. I've taken all > your advice (as best I've understood it) and created the most simple [quoted text clipped - 237 lines] >> >> Marc Marc Gravell - 21 Jan 2008 08:24 GMT Generally you would encapsulate the binding details to inside the user-control, just providing access to perhaps a DataSource and DataMember pair to obtain the record. If you know that you only need to support a single entity, then another option is to simply expose that entity on the user-control, as below with the Address property on the AddressControl.
I've also implemented change-notification on the Address entity so that you can verify (by the second Address control) that the change has committed itself. Let me know if I've missed the point of what you want, or if you want an explanation of any of it (I didn't have time for line-by-line comments).
Marc
using System; using System.ComponentModel; using System.Collections.Generic; using System.Windows.Forms; using System.Diagnostics;
public class Address : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected bool UpdateField<T>(ref T field, T value, string propertyName) { if(EqualityComparer<T>.Default.Equals(field, value)) return false; // update field and fire event field = value; if(!string.IsNullOrEmpty(propertyName) && PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); Trace.WriteLine(string.Format("{0}={1}", propertyName,field)); } return true; } private string _addressLine1, _zip; public string AddressLine1 { get { return _addressLine1; } set { UpdateField(ref _addressLine1, value, "AddressLine1");} } public string Zip { get { return _zip; } set { UpdateField(ref _zip, value, "Zip"); } } }
public class AddressControl : UserControl { public AddressControl() { tb1 = new TextBox(); tb2 = new TextBox(); tb1.Dock = tb2.Dock = DockStyle.Top; Controls.Add(tb1); Controls.Add(tb2); BorderStyle = BorderStyle.FixedSingle; Height = tb1.Height + tb2.Height; } private Address _address; TextBox tb1, tb2; public Address Address { get { return _address; } set { if (ReferenceEquals(Address, value)) return; tb1.DataBindings.Clear(); tb2.DataBindings.Clear(); _address = value; if (_address != null) { tb1.DataBindings.Add("Text", _address, "AddressLine1"); tb2.DataBindings.Add("Text", _address, "Zip"); } } } }
static class Program { static void Main() { Address a = new Address(); a.AddressLine1 = "Somewhere"; a.Zip = "ZONE 1"; Application.EnableVisualStyles(); using (Form f = new Form()) using (AddressControl ac1 = new AddressControl()) using (AddressControl ac2 = new AddressControl()) { ac1.Address = ac2.Address = a; ac1.Dock = DockStyle.Top; ac2.Dock = DockStyle.Bottom; f.Controls.Add(ac1); f.Controls.Add(ac2); Application.Run(f); } } }
Steve K. - 21 Jan 2008 08:46 GMT Hi Marc,
Thanks for the real quick reply, I'm eager to try out what you've supplied here.
First I just wanted to ask a few questions: 1) My original question on this thread (and apologies for letting the topic drift) was how to deal with a ComboBox and you suggested that I expose the required properties to the UserControl's interface. I liked your suggestion for the sole reason that it didn't tightly couple my UserControl to a specific entity. I could, in theory, bind my UserControl to ANY object that made sense. Now the situation has changed a bit and it looks like I've encountered a VERY OLD bug (I'm not one to scream bug, but after reading that web page a couple more times I think it's explaining exactly what I'm experiencing) with databinding more than one property in a UserControl. If my original request had been "How can I bind to 2 or more TextBox controls in a UserControl would you have still suggested I expose the properties to the UserControl interface? I ask because I"m trying to understand if what I'm trying to do is a "Heh, yeah.. of course that won't work, Kid. What did you expect?" or if my attempt to bind to multiple properties was sound? Did I misunderstand your earlier post when you suggested ( I thought ) that I exposes the properties for binding directly?
2) If I do handle the binding in the UserControl and expose a DataSource property (and other properties for my ComboBox 'member fields (God help me if I want two ComboBox controls on the UserControl or I think I will be in trouble again) to set the BindingSource.DataSource... is it possible to use an interface to create the BindingSource? I'm just thinking of ways to keep things loosely coupled and an interface-driven BindingSource seems to be the only way.
3) By any chance did you try to do things the way I currently have them setup (with the inner Control's Text properties exposed via proxies on the UserControl interface and if so did you experience the same behavior ( changes not sticking on any control other than the first)? I'd like to know if this is indeed a bug or if I've done something wrong. Not knowing is the worst!
Thanks again for the super fast reply, gonna snoop your code now and learn from it. :0)
(If I have found a bug and it's this old... it just might be enough to turn me into one of the Anti-Microsoft people that I've never understood very well... I hope not)
-Steve
> Generally you would encapsulate the binding details to inside the > user-control, just providing access to perhaps a DataSource and DataMember [quoted text clipped - 87 lines] > } > } Marc Gravell - 21 Jan 2008 11:10 GMT Note: you could also use the member to *obtain* a concrete address from the source (just passing in property names on the container, like "PrimaryAddress", "BackupAddress"; but this is more involved and requires knowledge of the currency manager and data context. Let me know if this is what you mean... i.e. so you can say:
BindingList<Person> people = ... addressCtrl.DataSource = people; addressCtrl.DataMember = "PrimaryAddress";
(to edit the PrimaryAddress of the currently selected Person in the list) marc
Steve K. - 21 Jan 2008 09:37 GMT Hi Marc,
I spent some time with your example code, it does indeed work just as I would expect. Nice sample, thank you. I also answered my question #2 about interfaces and yes, they work fine so that is good.
I still prefer the design of binding directly to a series of properties on the UserControl. It could be that I've spent so much time on it that I don't want to abandon it, I'm sure after a night's sleep I will have a different perspective.
Can you see any advantage.disadvantage to creating a BindingSource object in the UserControl and assigning the value of the Address property to the DataSource property of the BindingSource? I haven't read up on BindingSource much to know just what all it does, I have seen that you add Bindings to it the same way you do for a ControlBindingCollection... I don't need all the sorting/navigation stuff.
Just trying to think of any other options before I run with your solution (which I think I will).
Anyway, I need sleep. ;0|
Thanks again for the GREAT help, I really do appreciate it and will let you know how I make out with everything.
-Steve
> Generally you would encapsulate the binding details to inside the > user-control, just providing access to perhaps a DataSource and DataMember [quoted text clipped - 87 lines] > } > } Steve K. - 21 Jan 2008 10:46 GMT Sorry for all the messages! :0)
Marc, I've completed my analysis of my options and determined that the decision comes down to one thing: Coupling.
Your proposed solution is elegant and a month ago, before I started my "loose coupling" obsession is EXACTLY what I would have wanted. Clean interface, takes my business object directly and hides the implementation from the consuming control. Very nice.
The alternative (exposing all inner control properties through properties on the UserControl) is ugly, could have some code duplication and because of a bug in DataBinding will result in much more code (2 TextChanged) handlers for each control to bind to. However, it is loosely coupled.
I think I need to get over myself and not take this 'coupling' issue too far. I'm not developing a control library but rather an inhouse application that will likely never benefit from being loosely coupled. I'm stuck on the bandwagon... help! ;0)
Anyway, just wanted to update you. Thanks again, Steve
> Generally you would encapsulate the binding details to inside the > user-control, just providing access to perhaps a DataSource and DataMember [quoted text clipped - 87 lines] > } > } Marc Gravell - 21 Jan 2008 11:04 GMT > is it possible to use an interface to create the BindingSource? (but may answer some of the other questions) Kind of - you could quite happily provide an object DataSource and a pair of FooMember / BarMember strings (for the property-names - i.e."Zip" etc). I recommend "object DataSource" since this would map to what the IDE expects, and would allow for use with IList, IListSource, BindingSource, etc. Crude but it works.
All you'd do to change the current code is something like below; if you are binding to other controls, then you do it differently - for example, with a ComboBox you'd set DataSource and DisplayMember etc, rather than doing a DataBindings.Add; this is precisely why you only want to expose "source" and "member" properties on the API; so that if you change the internals the callers don't need to know. Theoretically you can have multiple {Named}DataSource properties, but the IDE may get even more confused than it does normally ;-p
// address unchanged
public class AddressControl : UserControl { public AddressControl() { tb1 = new TextBox(); tb2 = new TextBox(); tb1.Dock = tb2.Dock = DockStyle.Top; Controls.Add(tb1); Controls.Add(tb2); BorderStyle = BorderStyle.FixedSingle; Height = tb1.Height + tb2.Height; } private TextBox tb1, tb2; private object _dataSource; private string _fooMember, _barMember;
// add lots of attributes from typical DataSource public object DataSource { get { return _dataSource; } set { if (!ReferenceEquals(DataSource, value)) { _dataSource = value; Rebind(); } } }
// add lots of attributes from typical ValueMember public string FooMember { get { return _fooMember; } set { if (FooMember != value) { _fooMember = value; Rebind(); } } } // add lots of attributes from typical ValueMember public string BarMember { get { return _barMember; } set { if (BarMember != value) { _barMember = value; Rebind(); } } }
private void Rebind() { tb1.DataBindings.Clear(); tb2.DataBindings.Clear(); if (DataSource != null) { if (!string.IsNullOrEmpty(FooMember)) { tb1.DataBindings.Add("Text", DataSource, FooMember); } if (!string.IsNullOrEmpty(BarMember)) { tb2.DataBindings.Add("Text", DataSource, BarMember); } } } }
static class Program { static void Main() { Address a = new Address(); a.AddressLine1 = "Somewhere"; a.Zip = "ZONE 1"; Application.EnableVisualStyles(); using (Form f = new Form()) using (AddressControl ac1 = new AddressControl()) using (AddressControl ac2 = new AddressControl()) { ac1.DataSource = ac2.DataSource = a; ac1.FooMember = ac2.BarMember = "AddressLine1"; ac1.BarMember = ac2.FooMember = "Zip"; ac1.Dock = DockStyle.Top; ac2.Dock = DockStyle.Bottom; f.Controls.Add(ac1); f.Controls.Add(ac2); Application.Run(f); } } }
Free MagazinesGet these publications absolutely FREE for up to 12 months. There are no hidden fees and no obligation. Simply choose a title, complete the application form and submit it. Read more ...
|
|
|