WinForms BindingComplete is triggered by another property - c#

I have a TextBox whose Text property is bound to a ViewModel property called Name and a Button whose Enable property is bound to a ViewModel property called IsBusy.
And the TexBox Binding implements BindingComplete that changes the TextBox's background color if an exception is thrown by the property.
The problem is that BindingComplete is also raised when IsBusy property changes which the IsBusy property binding is not subscribing, and the BindingComplete event argument has the BindingField and BindingMember of "Name" and the associated control of this binding is TextBox.
Why IsBusy property is raising BindingComplete that is bound to the Name property?
Because of this, after I force TextBox binding to call WriteValue() to validate the property which sets the background color as expected when there is an error, and assign a true value to IsBusy property to indicate it is busy, it sets the background color back to White, because IsBusy raises TextBox's BindingComplete without an exception.
FYI, I'm intentionally raising PropertyChanged events and throwing exceptions because I want the incorrect values to be entered for users to review.
This is just a demonstration of the problem I'm having. I have more complex business requirements when errors occur. So, it would be greatly appreciated if you could provide an explanation of why this is happening and how to prevent it or fix it.
public partial class BindingTestForm : Form
{
private TestViewModel viewModel = new TestViewModel();
public BindingTestForm()
{
InitializeComponent();
var textBoxTextBinding = new Binding(nameof(TextBox.Text), viewModel, nameof(TestViewModel.Name), true, DataSourceUpdateMode.OnPropertyChanged);
textBoxTextBinding.BindingComplete += TextBoxTextBinding_BindingComplete;
NameTextBox.DataBindings.Add(textBoxTextBinding);
var enableBinding = new Binding(nameof(Control.Enabled), viewModel, nameof(TestViewModel.IsBusy));
enableBinding.Format += EnableBinding_Format;
SaveButton.DataBindings.Add(enableBinding);
Load += BindingTestForm_Load;
}
private void EnableBinding_Format(object sender, ConvertEventArgs e)
{
e.Value = !(bool)e.Value;
}
private void TextBoxTextBinding_BindingComplete(object sender, BindingCompleteEventArgs e)
{
if (e.Exception != null)
NameTextBox.BackColor = Color.Red;
else
NameTextBox.BackColor = Color.White;
}
private async void BindingTestForm_Load(object sender, EventArgs e)
{
viewModel.IsBusy = true;
await Task.Run(async () =>
{
await Task.Delay(2000);
});
viewModel.IsBusy = false;
}
public class TestViewModel: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _name;
public string Name
{
get => _name;
set
{
_name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TestViewModel.Name)));
if (string.IsNullOrWhiteSpace(_name))
throw new Exception("Please enter package name.");
if (_name.Length > 5)
throw new Exception("Max length 5.");
}
}
public bool _isBusy = false;
public bool IsBusy
{
get => _isBusy;
set
{
_isBusy = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TestViewModel.IsBusy)));
}
}
}
private async void SaveButton_Click(object sender, EventArgs e)
{
NameTextBox.DataBindings[nameof(TextBox.Text)].WriteValue();
viewModel.IsBusy = true;
await Task.Run(async () =>
{
await Task.Delay(2000);
});
viewModel.IsBusy = false;
}
}
partial class BindingTestForm
{
/// <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 Windows Form 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.NameTextBox = new System.Windows.Forms.TextBox();
this.SaveButton = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// NameTextBox
//
this.NameTextBox.Location = new System.Drawing.Point(13, 13);
this.NameTextBox.Name = "NameTextBox";
this.NameTextBox.Size = new System.Drawing.Size(100, 20);
this.NameTextBox.TabIndex = 0;
//
// SaveButton
//
this.SaveButton.Location = new System.Drawing.Point(13, 75);
this.SaveButton.Name = "SaveButton";
this.SaveButton.Size = new System.Drawing.Size(75, 23);
this.SaveButton.TabIndex = 1;
this.SaveButton.Text = "Save";
this.SaveButton.UseVisualStyleBackColor = true;
this.SaveButton.Click += new System.EventHandler(this.SaveButton_Click);
//
// BindingTestForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(800, 450);
this.Controls.Add(this.SaveButton);
this.Controls.Add(this.NameTextBox);
this.Name = "BindingTestForm";
this.Text = "FlowLayoutTestForm";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.TextBox NameTextBox;
private System.Windows.Forms.Button SaveButton;
}

I must confess that I don't know why this happens.
As an alternative solution, I suggest you to create a text box control being able to display a red border as well as a tooltip.
public class TextBoxEx : TextBox
{
[DllImport("user32")]
private static extern IntPtr GetWindowDC(IntPtr hwnd);
private const int WM_NCPAINT = 0x85;
private readonly ToolTip _toolTip = new ToolTip();
private bool _hasError;
private string _toolTipText;
public string ToolTipText
{
get {
return _toolTipText;
}
set {
if (value != _toolTipText) {
_toolTipText = value;
if (String.IsNullOrEmpty(_toolTipText)) {
_toolTip.Hide(this);
_hasError = false;
} else {
_toolTip.Show(_toolTipText, this, 3000);
_hasError = true;
}
// This is required to update the border color immediately.
var m = Message.Create(Handle, WM_NCPAINT, IntPtr.Zero, IntPtr.Zero);
WndProc(ref m);
}
}
}
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
if (_hasError && m.Msg == WM_NCPAINT) {
var dc = GetWindowDC(Handle);
using (Graphics g = Graphics.FromHdc(dc)) {
g.DrawRectangle(Pens.Red, 0, 0, Width - 1, Height - 1);
}
}
}
}
Setting the tooltip text to something different than null or empty show the tooltip and shows a red border.
The data model (view model in a MVVM context) must then provide an error message and implement INotifyPropertyChanged. Example:
public class Model : INotifyPropertyChanged
{
private string _text;
public string Text
{
get { return _text; }
set {
if (value != _text) {
_text = value;
OnPropertyChanged(nameof(Text));
OnPropertyChanged(nameof(TextErrorMessage));
}
}
}
public string TextErrorMessage
{
get {
return _text?.Length == 4
? null
: "Please enter a text of length 4";
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Now bind the Text property of the TextBoxEx to the Text property of the model and bind the ToolTipText property of the TextBoxEx to the TextErrorMessage property of the model. Set both bindings to OnPropertyChanged and assign a model object to the DataSource property of the BindingSource component on your form.
Now you have a full functional MVVM pattern requiring no code in the form other than assigning the model to the binding source.

Related

Why is it that unrelated properties are also being called multiple times in Winform databinding, and how to fix it?

This is my winform code:
partial class Form1
{
/// <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 Windows Form 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.textBox3 = new System.Windows.Forms.TextBox();
this.SuspendLayout();
//
// textBox1
//
this.textBox1.Location = new System.Drawing.Point(28, 129);
this.textBox1.Name = "textBox1";
this.textBox1.Size = new System.Drawing.Size(100, 20);
this.textBox1.TabIndex = 0;
this.textBox1.Leave += new System.EventHandler(this.textBox1_Leave);
//
// textBox2
//
this.textBox2.Location = new System.Drawing.Point(28, 227);
this.textBox2.Name = "textBox2";
this.textBox2.Size = new System.Drawing.Size(100, 20);
this.textBox2.TabIndex = 1;
//
// textBox3
//
this.textBox3.Location = new System.Drawing.Point(28, 283);
this.textBox3.Name = "textBox3";
this.textBox3.Size = new System.Drawing.Size(100, 20);
this.textBox3.TabIndex = 2;
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(579, 412);
this.Controls.Add(this.textBox1);
this.Controls.Add(this.textBox3);
this.Controls.Add(this.textBox2);
this.Name = "Form1";
this.Text = "Form1";
this.Load += new System.EventHandler(this.Form1_Load);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.TextBox textBox1;
private System.Windows.Forms.TextBox textBox2;
private System.Windows.Forms.TextBox textBox3;
}
public partial class Form1 : Form
{
private readonly Form1VM _vm;
public Form1()
{
InitializeComponent();
_vm = new Form1VM();
}
private void Form1_Load(object sender, EventArgs e)
{
BindControlsToVM();
}
private void BindControl(Control control, string propertyName)
{
control.DataBindings.Clear();
control.DataBindings.Add(nameof(control.Text), _vm, propertyName);
}
private void BindControlsToVM()
{
BindControl(textBox1, nameof(_vm.Name));
BindControl(textBox2, nameof(_vm.Surface));
BindControl(textBox3, nameof(_vm.Surface1));
}
private void textBox1_Leave(object sender, EventArgs e)
{
}
private void button1_Click(object sender, EventArgs e)
{
}
}
And this is my ViewModel ( I try to follow WPF in Winform)
public class Form1VM : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged(nameof(Name));
//OnPropertyChanged(nameof(Surface));
}
}
private string _surface;
public string Surface
{
get { return _surface; }
set
{
_surface = value;
OnPropertyChanged(nameof(Surface));
}
}
private string _surface1;
public string Surface1
{
get { return _surface1; }
set
{
_surface1 = value;
OnPropertyChanged(nameof(Surface1));
}
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
After the code compiles and runs, put the breakpoint at the get accessor at Name property. Now try to change either Surface or Surface1 properties at TextBox UI, you will find that Name property get accessor is also being invoked, multiple times even!
There is a performance issue with this kind of calling.
I've no idea why an unrelated property will get invoked when I change other properties, why is this so, and how to prevent it?
I've no idea why an unrelated property will get invoked when I change other properties, why is this so...
For the class Form1VM, each property is unrelated to the others and you also implemented INotifyPropertyChanged to provide change notification, so you would expect that the binding mechanism to be smart enough to only pull values that have posted a change notification.
Unfortunately, this is not the case and the default mechanism pulls all bound properties after it sends a change to the bound item. The default mechanism does monitor INotifyPropertyChanged.PropertyChanged event responds pulling all bound values instead of just the changed one.
This is all handled by a PropertyManager that is maintained by the BindingContext Property of the control's ContainerControl.
The observed behavior appears to be the result of the PropertyManager.OnCurrentChanged Method that calls BindingManagerBase.PushData that ultimately results in an iteration over the bindings and calling Binding.PushData where the following code executes and retrieves the underlying data source value.
if (IsBinding) {
dataSourceValue = bindToObject.GetValue();
object controlValue = FormatObject(dataSourceValue);
SetPropValue(controlValue);
modified = false;
}
The subject code is declaring the binding such that the above sequence is triggered by the TextBox.Validating event. When the underlying datasource (_vm) raises the PropertyChanged event, the sequence begins again with the PropertyManager.OnCurrentChanged method.
how to prevent it?
You could create a derived PropertyManager class that overrides OnCurrentChanged method and code your own behavior. To get this custom class to be used, you would also need to create a custom BindingContext class to install it. This is not something that I would recommend if you can accept a single polling of bound properties on changes propagated by the data binding mechanism. This behavior can be accomplished by using a BindingSource as an intermediary between _vm and the binding.
The following shows the changes to the posted code necessary to use a BindingSource.
private BindingSource bs = new BindingSource();
public Form1()
{
InitializeComponent();
_vm = new Form1VM();
bs.DataSource = _vm;
}
private void BindControl(Control control, string propertyName)
{
control.DataBindings.Clear();
control.DataBindings.Add(nameof(control.Text), bs, propertyName, true, DataSourceUpdateMode.OnValidation);
}
Another alternative is for Form1VM to implement the ICurrencyManagerProvider interface and provide a custom CurrencyManager Class implementation similar to the way the BindingSource class does. This is something that I have never attempted, but I suspect it would be a similar task to deriving a custom PropertyManager.

Problems with user controls and Tab order

I've created a project with multiple user controls to support transparency, gradient and themes for winforms.
I was looking for a way to create a replacement for textbox, since winforms textboxes are not using the regular OnPaint and OnPaintBackground that other winforms control use, and sure enough, I've found something I can work with right here on stackoverflow. Brian's comment gave me the solution - Wrapping a transparent RichTextBox inside my own control.
However, this posed a new problem that I can't figure out how to solve - The TabIndex property dosn't operate as expected.
With normal textboxes, when you have multiple textboxes and each one have a different tab index, the focus goes from one textbox to the other in the order specified by the tab index. In my case, it doesn't. Instead, it's unpredictable.
I've tried multiple forms with different layouts and controls on them, but I can't seem to find any predictable pattern of behavior that would suggest the problem.
Here is the relevant control's code (the parent, ZControl inherits UserControl if that matters):
/// <summary>
/// A stylable textbox.
/// <Remarks>
/// The solution for writing a stylable textbox was inspired by this SO post and Brian's comment:
/// https://stackoverflow.com/a/4360341/3094533
/// </Remarks>
/// </summary>
[DefaultEvent("TextChanged")]
public partial class ZTextBox : ZControl
{
#region ctor
public ZTextBox()
{
TextBox = new TransparentRichTextBox();
TextBox.BackColor = Color.Transparent;
TextBox.BorderStyle = BorderStyle.None;
TextBox.Multiline = false;
TextBox.TextChanged += TextBox_TextChanged;
TextBox.TabStop = true;
TextBox.AcceptsTab = false;
InitializeComponent();
AdjustTextBoxRectangle();
this.Controls.Add(TextBox);
this.RoundedCorners.PropertyChanged += RoundedCorners_PropertyChanged;
}
#endregion ctor
#region properties
private TransparentRichTextBox TextBox { get; }
public override string Text
{
get
{
return TextBox.Text;
}
set
{
TextBox.Text = value;
}
}
[DefaultValue(false)]
public bool Multiline
{
get
{
return this.TextBox.Multiline;
}
set
{
this.TextBox.Multiline = value;
}
}
public override Font Font
{
get
{
return base.Font;
}
set
{
if (base.Font != value)
{
base.Font = value;
if (TextBox != null)
{
TextBox.Font = value;
}
}
}
}
public new int TabIndex
{
get
{
return this.TextBox.TabIndex;
}
set
{
this.TextBox.TabIndex = value;
}
}
#region hidden properties
[
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden),
Browsable(false),
EditorBrowsable(EditorBrowsableState.Never)
]
public override Color ForeColor
{
get
{
return TextBox.ForeColor;
}
set
{
TextBox.ForeColor = value;
}
}
[
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden),
Browsable(false),
EditorBrowsable(EditorBrowsableState.Never)
]
public override ContentAlignment TextAlign
{
get
{
return base.TextAlign;
}
set
{
base.TextAlign = value;
}
}
[
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden),
Browsable(false),
EditorBrowsable(EditorBrowsableState.Never)
]
public override Point TextLocationOffset
{
get
{
return base.TextLocationOffset;
}
set
{
base.TextLocationOffset = value;
}
}
#endregion hidden properties
#endregion properties
#region methods
protected override void OnGotFocus(EventArgs e)
{
base.OnGotFocus(e);
TextBox.Focus();
}
protected override void DrawText(Graphics graphics, string text, ContentAlignment textAlign, Point locationOffset, Size stringSize)
{
// Do nothing - The transparent rich textbox is responsible for drawing the text...
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
AdjustTextBoxRectangle();
}
private void AdjustTextBoxRectangle()
{
var corners = this.RoundedCorners.Corners;
var leftAdjustment = ((corners & RoundedEdges.TopLeft) == RoundedEdges.TopLeft || (corners & RoundedEdges.BottomLeft) == RoundedEdges.BottomLeft) ? this.RoundedCorners.ArcSize / 2 : 0;
var rightAdjustment = ((corners & RoundedEdges.TopRight) == RoundedEdges.TopRight || (corners & RoundedEdges.BottomRight) == RoundedEdges.BottomRight) ? this.RoundedCorners.ArcSize / 2 : 0;
TextBox.Top = 0;
TextBox.Left = leftAdjustment;
TextBox.Width = this.Width - leftAdjustment - rightAdjustment;
TextBox.Height = this.Height;
}
#endregion methods
#region event handlers
private void RoundedCorners_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
AdjustTextBoxRectangle();
}
private void TextBox_TextChanged(object sender, EventArgs e)
{
OnTextChanged(e);
}
#endregion event handlers
#region private classes
private class TransparentRichTextBox : RichTextBox
{
public TransparentRichTextBox()
{
this.SetStyle(ControlStyles.SupportsTransparentBackColor, true);
this.SetStyle(ControlStyles.Opaque, true);
this.SetStyle(ControlStyles.OptimizedDoubleBuffer, false);
}
protected override CreateParams CreateParams
{
get
{
CreateParams parms = base.CreateParams;
parms.ExStyle |= 0x20; // Turn on WS_EX_TRANSPARENT
return parms;
}
}
}
#endregion private classes
}
And the designer code, if that's relevant:
partial class ZTextBox
{
/// <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.SuspendLayout();
//
// ZTextBox
//
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
this.Name = "ZTextBox";
this.RoundedCorners.ArcSize = 50;
this.RoundedCorners.Corners = Zohar.UserControls.RoundedEdges.None;
this.Size = new System.Drawing.Size(100, 20);
this.Style.DisabledStyle.BackColor = System.Drawing.Color.Empty;
this.Style.DisabledStyle.BackgroundImage = null;
this.Style.DisabledStyle.BorderColor = System.Drawing.Color.Empty;
this.Style.DisabledStyle.ForeColor = System.Drawing.Color.Empty;
this.Style.DisabledStyle.Gradient.Angle = 0F;
this.Style.DisabledStyle.Gradient.BackColor = System.Drawing.Color.Empty;
this.Style.DisabledStyle.Image = null;
this.Style.DisabledStyle.Name = null;
this.Style.EnabledStyle.BackColor = System.Drawing.Color.Empty;
this.Style.EnabledStyle.BackgroundImage = null;
this.Style.EnabledStyle.BorderColor = System.Drawing.Color.Empty;
this.Style.EnabledStyle.ForeColor = System.Drawing.Color.Empty;
this.Style.EnabledStyle.Gradient.Angle = 0F;
this.Style.EnabledStyle.Gradient.BackColor = System.Drawing.Color.Empty;
this.Style.EnabledStyle.Image = null;
this.Style.EnabledStyle.Name = null;
this.Style.HoverStyle.BackColor = System.Drawing.Color.Empty;
this.Style.HoverStyle.BackgroundImage = null;
this.Style.HoverStyle.BorderColor = System.Drawing.Color.Empty;
this.Style.HoverStyle.ForeColor = System.Drawing.Color.Empty;
this.Style.HoverStyle.Gradient.Angle = 0F;
this.Style.HoverStyle.Gradient.BackColor = System.Drawing.Color.Empty;
this.Style.HoverStyle.Image = null;
this.Style.HoverStyle.Name = null;
this.ResumeLayout(false);
}
#endregion
}
The issue is caused by the following code:
public new int TabIndex
{
get
{
return this.TextBox.TabIndex;
}
set
{
this.TextBox.TabIndex = value;
}
}
You should never do this for UserControl (actually for any control). The documentation for Control.TabIndex property states:
Gets or sets the tab order of the control within its container.
In other words, the control TabIndex property is not global for the form, but scoped to the control container (parent).
The effect is that the form designer code where your control resides will call the shadow TabOrder setter, but the tab navigation handling will simply call the base Control property, leading to undetermined behavior.
Also note that setting the TabIndex of the inner TextBox makes no any sense since it's the only control inside the container (your control). While what you really need is to set the TabIndex of your control inside its container.
With that being said, simply remove the above code and everything will work as expected.
that's probably because you have not added your textbox's in order or maybe you deleted some of them while adding them, anyway you can choose the order on the properties of the controls => TabIndex

Overrided Text property of a Textbox does not refresh properly

I'm creating a custom control (a watermarked textbox) and it inherits from Textbox. As of now, the textbox correctly shows the watermark on losing focus when there's no text and deletes it when the textbox gets focus (it even changes the text's color if it's the watermark). What I want it to do is to report that it has no text when it's showing the watermark, so I'm trying to override the Text property.
Code as follows:
public class WatermarkedTextbox : TextBox
{
private bool _isWatermarked;
private string _watermark;
public string Watermark
{
get { return _watermark; }
set { _watermark = value; }
}
[Bindable(false), EditorBrowsable(EditorBrowsableState.Never), Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public override string Text
{
get
{
return _isWatermarked ? string.Empty : base.Text;
}
set
{
base.Text = value;
}
}
public WatermarkedTextbox()
{
GotFocus += WatermarkedTextbox_GotFocus;
LostFocus += WatermarkedTextbox_LostFocus;
}
private void WatermarkedTextbox_LostFocus(object sender, EventArgs e)
{
if (Text.Length == 0)
{
ForeColor = SystemColors.InactiveCaption;
Text = _watermark;
_isWatermarked = true;
}
}
private void WatermarkedTextbox_GotFocus(object sender, EventArgs e)
{
if (_isWatermarked)
{
ForeColor = SystemColors.ControlText;
Text = string.Empty;
_isWatermarked = false;
}
}
}
Problem is, when the textbox gets focus it does not delete the watermark.
What am I missing/doing wrong here?
Ah sorry I didn't read that clearly. Alternatively you might want to notify not by overriding the Text Property.
You can make use of event as such:
public class WatermarkedTextbox : TextBox, INotifyPropertyChanged
{
private bool _isWatermarked;
private string _watermark;
public string Watermark
{
get { return _watermark; }
set { _watermark = value; }
}
public bool IsWaterMarked
{
get
{
return _isWatermarked;
}
set
{
_isWatermarked = value;
OnPropertyChanged("IsWaterMarked");
}
}
public WatermarkedTextbox()
{
GotFocus += WatermarkedTextbox_GotFocus;
LostFocus += WatermarkedTextbox_LostFocus;
}
private void WatermarkedTextbox_LostFocus(object sender, EventArgs e)
{
if (Text.Length == 0)
{
ForeColor = SystemColors.InactiveCaption;
Text = _watermark;
IsWaterMarked = true;
}
}
private void WatermarkedTextbox_GotFocus(object sender, EventArgs e)
{
if (_isWatermarked)
{
ForeColor = SystemColors.ControlText;
Text = string.Empty;
IsWaterMarked = false;
}
}
protected void OnPropertyChanged(PropertyChangedEventArgs e)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, e);
}
protected void OnPropertyChanged(string propertyName)
{
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
Then on the Main Form, you can subscribe and add a handler to the PropertyChanged event:
//somewhere, like in the forms constructor, you need to subscribe to this event
watermarkedTextbox2.PropertyChanged += watermarkedTextbox2_PropertyChanged;
// the handler function
void watermarkedTextbox2_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "IsWaterMarked")
{
if (watermarkedTextbox2.IsWaterMarked)
; //handle here
else
; //handle here
}
}
Windows supports watermarking for text boxes (and other edit controls such as combo boxes), they call it the "cue banner". Note however that this does not work for multi-line text boxes.
Setting the cue banner on a supported control is simply a matter of using the Win32 API to send an EM_SETCUEBANNER message to the control containing the watermark text. Windows will then handle detecting when the control is empty or has focus and do all the hard work for you, you won't need to use events to manage state. The cue banner is also ignored when you get the control's Text property.
I use the following helper class to set the cue banner (works for combo boxes too):
public class CueBannerHelper
{
#region Win32 API's
[StructLayout(LayoutKind.Sequential)]
public struct COMBOBOXINFO
{
public int cbSize;
public RECT rcItem;
public RECT rcButton;
public IntPtr stateButton;
public IntPtr hwndCombo;
public IntPtr hwndItem;
public IntPtr hwndList;
}
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int left;
public int top;
public int right;
public int bottom;
}
/// <summary>Used to get the current Cue Banner on an edit control.</summary>
public const int EM_GETCUEBANNER = 0x1502;
/// <summary>Used to set a Cue Banner on an edit control.</summary>
public const int EM_SETCUEBANNER = 0x1501;
[DllImport("user32.dll")]
public static extern bool GetComboBoxInfo(IntPtr hwnd, ref COMBOBOXINFO pcbi);
[DllImport("user32.dll")]
public static extern Int32 SendMessage(IntPtr hWnd, int msg, int wParam, [MarshalAs(UnmanagedType.LPWStr)] string lParam);
#endregion
#region Method members
public static void SetCueBanner(Control control, string cueBanner) {
if (control is ComboBox) {
CueBannerHelper.COMBOBOXINFO info = new CueBannerHelper.COMBOBOXINFO();
info.cbSize = Marshal.SizeOf(info);
CueBannerHelper.GetComboBoxInfo(control.Handle, ref info);
CueBannerHelper.SendMessage(info.hwndItem, CueBannerHelper.EM_SETCUEBANNER, 0, cueBanner);
}
else {
CueBannerHelper.SendMessage(control.Handle, CueBannerHelper.EM_SETCUEBANNER, 0, cueBanner);
}
}
#endregion
}
All that is then required to implement the watermark on the custom TextBox control is the following property (the attributes at the top are for the control's design-time properties) :
/// <summary>
/// Gets or sets the watermark that the control contains.
/// </summary>
[Description("The watermark that the control contains."),
Category("Appearance"),
DefaultValue(null),
Browsable(true)
]
public string Watermark {
get { return this._watermark; }
set {
this._watermark = value;
CueBannerHelper.SetCueBanner(this, value);
}
}
Delete your overriden Text property, then it will work!
Delete these lines:
[Bindable(false), EditorBrowsable(EditorBrowsableState.Never), Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public override string Text
{
get
{
return _isWatermarked ? string.Empty : base.Text;
}
set
{
base.Text = value;
}
}
Hans Passant's comment was the correct answer to my question. Also, thanks to everyone that took time to offer help.
I finally decided to go the simplest route (handling PropertyChanged seems too convoluted for this particular need and hooking Windows APIs leaves out multiline textboxes so it's not an option).
In case someone needs it, here's the code:
public class WatermarkedTextbox : TextBox
{
private bool _isWatermarked;
private string _watermark;
public string Watermark
{
get { return _watermark; }
set { _watermark = value; }
}
[Bindable(false), EditorBrowsable(EditorBrowsableState.Never), Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public override string Text
{
get
{
return _isWatermarked ? string.Empty : base.Text;
}
set
{
base.Text = value;
}
}
public WatermarkedTextbox()
{
GotFocus += WatermarkedTextbox_GotFocus;
LostFocus += WatermarkedTextbox_LostFocus;
}
private void WatermarkedTextbox_LostFocus(object sender, EventArgs e)
{
if (Text.Length == 0)
{
_isWatermarked = true;
ForeColor = SystemColors.InactiveCaption;
Text = _watermark;
}
}
private void WatermarkedTextbox_GotFocus(object sender, EventArgs e)
{
if (_isWatermarked)
{
_isWatermarked = false;
ForeColor = SystemColors.ControlText;
Text = string.Empty;
}
}
}

sorted combo box and reassigning the datasource

Background:
There are two combo boxes that differ only in the Sorted property. comboBox1 has the Sorted property set to true and comboBox2 has the Sorted property set to false. When attempting to reassign/reset the datasource property of these two combo boxes, comboBox1 displays no data and comboBox2 does. Why does the Sorted property prevent comboBox1 from displaying its' data properly?
All code included below:
public partial class Form1 : Form
{
private string[] a8BitGames = { "Metroid", "Zelda", "Phantasy Star", "SB:S&SEP" };
private string[] a16BitGames = { "StarFox", "Link", "Final Fantasy", "Altered Beast" };
private List<string> lSomeList = null;
private List<string> lSomeOtherList = null;
public Form1()
{
InitializeComponent();
this.lSomeList = new List<string>(a8BitGames);
this.lSomeOtherList = new List<string>(a16BitGames);
this.comboBox1.DataSource = lSomeList;
this.comboBox2.DataSource = lSomeOtherList;
}
private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
this.IndexChanged(1);
}
private void IndexChanged(int comboBox)
{
this.comboBox1.DataSource = null;
this.comboBox1.DataSource = a16BitGames;
this.comboBox2.DataSource = null;
this.comboBox2.DataSource = a8BitGames;
}
private void comboBox2_SelectedIndexChanged(object sender, EventArgs e)
{
this.IndexChanged(2);
}
}
partial class Form1
{
/// <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 Windows Form 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.comboBox1 = new System.Windows.Forms.ComboBox();
this.comboBox2 = new System.Windows.Forms.ComboBox();
this.SuspendLayout();
//
// comboBox1
//
this.comboBox1.FormattingEnabled = true;
this.comboBox1.Location = new System.Drawing.Point(13, 13);
this.comboBox1.Name = "comboBox1";
this.comboBox1.Size = new System.Drawing.Size(121, 21);
this.comboBox1.Sorted = true;
this.comboBox1.TabIndex = 0;
this.comboBox1.SelectedIndexChanged += new System.EventHandler(this.comboBox1_SelectedIndexChanged);
//
// comboBox2
//
this.comboBox2.FormattingEnabled = true;
this.comboBox2.Location = new System.Drawing.Point(13, 41);
this.comboBox2.Name = "comboBox2";
this.comboBox2.Size = new System.Drawing.Size(121, 21);
this.comboBox2.TabIndex = 1;
this.comboBox2.SelectedIndexChanged += new System.EventHandler(this.comboBox2_SelectedIndexChanged);
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(284, 262);
this.Controls.Add(this.comboBox2);
this.Controls.Add(this.comboBox1);
this.Name = "Form1";
this.Text = "Form1";
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.ComboBox comboBox1;
private System.Windows.Forms.ComboBox comboBox2;
}
Are you accidentally hiding an exception? According to MSDN you will receive an "ArgumentException" when "An attempt was made to sort a ComboBox that is attached to a data source."
http://msdn.microsoft.com/en-us/library/system.windows.forms.combobox.sorted.aspx
Try to sort by sorting the list on setting the DataSource
public List<string> A16Games
{
get { return this.a16BitGames.OrderBy(x => x).ToList(); }
}
public List<string> A8Games
{
get { return this.a8BitGames.OrderBy(x => x).ToList(); }
}
this.comboBox1.DataSource = this.A16Games;
this.comboBox2.DataSource = this.A8Games;
The problem is that You can not change data source of sorted combobox.
Here is excerpt of the code of ComboBox control:
protected override void OnDataSourceChanged(EventArgs e)
{
if ((this.Sorted && (this.DataSource != null)) && base.Created)
{
this.DataSource = null;
throw new InvalidOperationException(System.Windows.Forms.SR.GetString("ComboBoxDataSourceWithSort"));
}
...
...
}
In such case You should have received an InvalidOperationException.
Why didn't You receive ?
Here is the answer:
Implementation of DataSource property in ComboBox control redirects to its base class (ListControl) implementation:
public object DataSource
{
get
{
return base.DataSource;
}
set
{
base.DataSource = value;
}
}
And then in the base class's DataSource:
public object DataSource
{
[TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
get
{
return this.dataSource;
}
set
{
if (((value != null) && !(value is IList)) && !(value is IListSource))
{
throw new ArgumentException(System.Windows.Forms.SR.GetString("BadDataSourceForComplexBinding"));
}
if (this.dataSource != value)
{
try
{
this.SetDataConnection(value, this.displayMember, false);
}
catch
{
this.DisplayMember = "";
}
if (value == null)
{
this.DisplayMember = "";
}
}
}
}
Please, pay attention to the silent catch block.
Since SetDataConnection calls OnDataSourceChanged then as far as my analysis is correct this is the reason. If I'm wrong, please correct me.

Data binding not updating

I created a simple example with data binding (unfortunately we have a similar case in our system). I created a funky combo box:
public class FunkyComboBox : ComboBox
{
private object currentValue = null;
public FunkyComboBox()
{
if (LicenseManager.UsageMode == LicenseUsageMode.Runtime)
this.Items.Add("Other...");
}
protected override void OnSelectedIndexChanged(EventArgs e)
{
if (!this.Text.StartsWith("Other") && currentValue != this.SelectedItem)
{
currentValue = this.SelectedItem;
BindingManagerBase bindingManager = DataManager;
base.OnSelectedIndexChanged(e);
}
}
protected override void OnSelectionChangeCommitted(EventArgs e)
{
string itemAsStr = this.SelectedItem != null ? SelectedItem.ToString() : "";
if (itemAsStr.StartsWith("Other"))
{
string newItem = "item" + this.Items.Count;
if (!Items.Contains(newItem))
{
Items.Add(newItem);
}
SelectedItem = newItem;
}
else
{
OnSelectedIndexChanged(e); //forces a selectedIndexChanged event to be thrown
base.OnSelectionChangeCommitted(e);
}
}
}
Which adds new items when you click Other (in our system it opens a form where you can query the database, etc). Then I have a simple data object:
public class MyClass
{
private string value;
public string MyData
{
get{ return value;}
set{ this.value = value;}
}
}
And a test form with two controls bound to this object (some designer code removed):
public partial class Form1 : Form
{
MyClass myObj = new MyClass();
public Form1()
{
InitializeComponent();
myObj.MyData = "Nothing";
myClassBindingSource.DataSource = myObj;
}
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.textBox1 = new System.Windows.Forms.TextBox();
this.myClassBindingSource = new System.Windows.Forms.BindingSource(this.components);
this.funkyComboBox1 = new DataBindingTests.FunkyComboBox();
((System.ComponentModel.ISupportInitialize)(this.myClassBindingSource)).BeginInit();
this.SuspendLayout();
//
// textBox1
//
this.textBox1.DataBindings.Add(new System.Windows.Forms.Binding("Text", this.myClassBindingSource, "MyData", true, System.Windows.Forms.DataSourceUpdateMode.OnPropertyChanged));
//
// myClassBindingSource
//
this.myClassBindingSource.DataSource = typeof(DataBindingTests.MyClass);
//
// funkyComboBox1
//
this.funkyComboBox1.DataBindings.Add(new System.Windows.Forms.Binding("SelectedItem", this.myClassBindingSource, "MyData", true, System.Windows.Forms.DataSourceUpdateMode.OnPropertyChanged));
this.funkyComboBox1.DataBindings.Add(new System.Windows.Forms.Binding("SelectedValue", this.myClassBindingSource, "MyData", true, System.Windows.Forms.DataSourceUpdateMode.OnPropertyChanged));
//
// Form1
//
this.Controls.Add(this.textBox1);
this.Controls.Add(this.funkyComboBox1);
((System.ComponentModel.ISupportInitialize)(this.myClassBindingSource)).EndInit();
this.ResumeLayout(false);
this.PerformLayout();
}
private FunkyComboBox funkyComboBox1;
private System.Windows.Forms.BindingSource myClassBindingSource;
private System.Windows.Forms.TextBox textBox1;
}
If you run this code and start playing with the combo box, you will notice that the edit box changes only if you click on it. After every change a null value is set to my object and the text box is cleared. How can I make it set the correct value after every change?
I'm not really sure why the ComboBox data binding behaves this way, but I have found a workaround. It seems as though the databinding doesn't work correctly if you don't use a datasource for your ComboBox's values.
I made a few minor changes to FunkyComboBox, and it now works as expected.
public class FunkyComboBox : ComboBox
{
private object currentValue = null;
private List<string> innerItems = new List<string>();
public FunkyComboBox()
{
if (LicenseManager.UsageMode == LicenseUsageMode.Runtime)
innerItems.Add("Other...");
this.DataSource = innerItems;
}
protected override void OnSelectedIndexChanged(EventArgs e)
{
if (!this.Text.StartsWith("Other") && currentValue != this.SelectedItem)
{
currentValue = this.SelectedItem;
BindingManagerBase bindingManager = DataManager;
base.OnSelectedIndexChanged(e);
}
}
protected override void OnSelectionChangeCommitted(EventArgs e)
{
string itemAsStr = this.SelectedItem != null ? SelectedItem.ToString() : "";
if (itemAsStr.StartsWith("Other"))
{
string newItem = "item" + this.Items.Count;
if(!innerItems.Contains(newItem))
{
innerItems.Add(newItem);
this.RefreshItems();
} SelectedItem = newItem;
}
else
{
OnSelectedIndexChanged(e);
//forces a selectedIndexChanged event to be thrown
base.OnSelectionChangeCommitted(e);
}
}
}
It seems to be a bug with the base ComboBox as well. It is not possible to get this binding source craziness to work correctly.

Categories