Overrided Text property of a Textbox does not refresh properly - c#

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;
}
}
}

Related

WinForms BindingComplete is triggered by another property

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.

Moving Strings between UserControls not Working

I am trying to pass strings between forms. Why does it not? Am I missing something or is it an error in the program or what?
On UserControl3
UserControl1 u1;
public UserControl3()
{
u1 = new UserControl1();
InitializeComponent();
}
On UserControl3
public void materialCheckBox1_CheckedChanged(object sender, EventArgs e)
{
if (materialCheckBox1.Checked)
{
u1.toUserControl3 = "GOINTHEBOX!";
}
else
{
u1.toUserControl3 = string.Empty;
}
}
On UserControl1
public string toUserControl3
{
get
{
return textBox1.Text;
}
set
{
textBox1.Text = value;
}
}
On UserControl1
public void textBox1_TextChanged(object sender, EventArgs e)
{
}
Changing the Text property on a control through a piece of code doesn't necessarily mean the value control will update. Typically you need some sort of binding between your property, in this case toUserControl3, and your control. You need a way to tell your control that value changed so it knows to update.
You could accomplish databinding in the following way:
Create a new class to handle state and binding: This eliminated any need to pass controls into constructors of other controls.
public class ViewModel : INotifyPropertyChanged
{
public string TextBoxText => CheckBoxChecked ? "GOINTOTHEBOX!" : string.Empty;
public bool CheckBoxChecked
{
get { return _checkBoxChecked; }
set
{
_checkBoxChecked = value;
OnPropertyChanged("CheckBoxChecked");
}
}
private bool _checkBoxChecked;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
This is your main form
public void Form1
{
public Form1(ViewModel viewModel)
{
UserControl1.DataBindings.Add("TextBoxTextProperty", viewModel, "TextBoxText");
UserControl3.DataBindings.Add("MaterialCheckBoxCheckedProperty", viewModel, "CheckBoxChecked");
}
}
UserControl1
public void UserControl1()
{
public string TextBoxTextProperty
{
get { return textBox1.Text; }
set { textBox1.Text = value; }
}
}
UserControl3
public void UserControl3()
{
public bool MaterialCheckBoxCheckedProperty
{
get { return materialCheckBox1.Checked; }
set { materialCheckBox1.Checked = value; }
}
}

USer Control Custom event and set get property

I have 2 labels and a property in user control:
Here is property:
private int _SelectIndex;
[Browsable(true)]
public int SelectIndex { get; set; }
and 2 labels:
Label lbl1, lbl2;
void iControl()
{
lbl1 = new Label();
lbl2 = new Label();
lbl1.Name = "lbl1";
lbl2.Name = "lbl2";
lbl1.Click += lbl_Click;
lbl2.Click += lbl_Click;
this.Controls.Add(lbl1);
this.Controls.Add(lbl2);
}
Click:
void lbl_Click(object sender, EventArgs e)
{
Label selectedlbl = sender as Label;
if(selectedlbl.Name == "lbl1")
this.Select = 1;
else
this.Select = 2;
}
Class Event:
public class SelectEventArgs : EventArgs
{
private int index;
public SelectEventArgs(int index)
{
this.index = index;
}
public int ItemIndex
{
get
{
return index;
}
}
}
Custom event in my control:
public event EventHandler SelectEvent;
protected virtual void OnSelectEvent()
{
if (SelectEvent!= null)
SelectEvent(this, new SelectEventArgs(this._SelectIndex));
}
I need an event to get and set property value in MainForm as following:
int index = 0;
public Form1()
{
InitializeComponent();
this.icontrol = new iControl();
this.SelectEvent += Select();
}
void Select(object sender, SelectItem e)
{
//use this to set value of Select
this.icontrol.SelectIndex = index;
//and this to get value of Select
index = this.icontrol.SelectIndex;
}
Select is empty.
How to get it to work?
I post here for any one need it:
1.Declare a delegate:
public delegate void SelectIndexEventHandler(object sender, SelectEventArgs e);
public class SelectEventArgs : EventArgs
{
private int index;
public SelectEventArgs(int index)
{
this.index = index;
}
public int ItemIndex
{
get { return index; }
set { index = value; }
}
}
2. declare an event SelectIndexChanged and a method OnSelectIndexChanged:
public event SelectIndexEventHandler SelectIndexChanged;
protected virtual void OnSelectIndexChanged(SelectEventArgs e)
{
if (SelectIndexChanged != null)
SelectIndexChanged(this, e);
}
3.Call it in setter:
public int SelectIndex
{
get { return _SelectIndex; }
set {
_SelectIndex = value;
OnSelectIndexChanged(new SelectEventArgs(value));
}
}
and then MainForm:
this.gListBox1.SelectIndexChanged += icontrol_SelectIndexChanged;
void icontrol_SelectIndexChanged(object sender, SelectEventArgs e)
{
var current = e.ItemIndex;
}
thank again jbmintjb Reza Aghaei.
The code has multiple issues. Consider these tips to solve the issues:
SelecetEvent does't belong to the Form. The event belongs to icontrol.
this.SelectEvent += Select(); is incorrect, you should use:
icontrol.SelectEvent += Select;
When you have a custom event args, you should define the event this way:
public event EventHandler<SelectEventArgs> SelectEvent;
You should raise the event in setter of your property, using OnSelectEvent method which you created.
To learn more about events take a look at C# Handling and Raising Events.
Take a look at the SelectedIndexChanged event on the listbox control, think that is what you are looking for

How to update a ListBox if an element was changed c#

Hi,
I'm struggling a bit using the ListBox.DataSource and the INotifyPropertyChanged Interface. I checked several posts about this issue already but I cannot figure out, how to update the view of a ListBox if an element of the bound BindingList is changed.
I basically want to change the color of an IndexItem after the content has been parsed.
Here the relevant calls in my form:
btn_indexAddItem.Click += new EventHandler(btn_indexAddItem_Click);
lst_index.DataSource = Indexer.Items;
lst_index.DisplayMember = "Url";
lst_index.DrawItem += new DrawItemEventHandler(lst_index_DrawItem);
private void btn_indexAddItem_Click(object sender, EventArgs e)
{
Indexer.AddSingleURL(txt_indexAddItem.Text);
}
private void lst_index_DrawItem(object sender, DrawItemEventArgs e)
{
IndexItem item = lst_index.Items[e.Index] as IndexItem;
if (item != null)
{
e.DrawBackground();
SolidBrush brush = new SolidBrush((item.hasContent) ? SystemColors.WindowText : SystemColors.ControlDark);
e.Graphics.DrawString(item.Url, lst_index.Font, brush, 0, e.Index * lst_index.ItemHeight);
e.DrawFocusRectangle();
}
}
Indexer.cs:
class Indexer
{
public BindingList<IndexItem> Items { get; }
private object SyncItems = new object();
public Indexer()
{
Items = new BindingList<IndexItem>();
}
public void AddSingleURL(string url)
{
IndexItem item = new IndexItem(url);
if (!Items.Contains(item))
{
lock (SyncItems)
{
Items.Add(item);
}
new Thread(new ThreadStart(() =>
{
// time consuming parsing
Thread.Sleep(5000);
string content = item.Url;
lock (SyncItems)
{
Items[Items.IndexOf(item)].Content = content;
}
}
)).Start();
}
}
}
IndexItem.cs
class IndexItem : IEquatable<IndexItem>, INotifyPropertyChanged
{
public int Key { get; }
public string Url { get; }
public bool hasContent { get { return (_content != null); } }
private string _content;
public string Content {
get
{
return (hasContent) ? _content : "empty";
}
set
{
_content = value;
ContentChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void ContentChanged()
{
if (this.PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Content"));
}
}
public IndexItem(string url)
{
this.Key = url.GetHashCode();
this.Url = url;
}
public override bool Equals(object obj)
{
return Equals(obj as IndexItem);
}
public override int GetHashCode()
{
return Key;
}
public bool Equals(IndexItem other)
{
if (other == null) return false;
return (this.Key.Equals(other.Key)) ||
((hasContent || other.hasContent) && (this._content.Equals(other._content)));
}
public override string ToString()
{
return Url;
}
}
Any ideas what went wrong and how to fix it? I'll appreciate any hint...
It seems to me that the control should redraw when it raises the ListChanged event for that item. This will force it to do so:
lst_index.DrawItem += new DrawItemEventHandler(lst_index_DrawItem);
Indexer.Items.ListChanged += Items_ListChanged;
private void Items_ListChanged(object sender, ListChangedEventArgs e)
{
lst_index.Invalidate(); // Force the control to redraw when any elements change
}
So why doesn't it do that already? Well, it seems that the listbox only calls DrawItem if both DisplayMember changed, and if the INotifyPropertyChanged event was raised from the UI thread. So this also works:
lock (SyncItems)
{
// Hacky way to do an Invoke
Application.OpenForms[0].Invoke((Action)(() =>
{
Items[Items.IndexOf(item)].Url += " "; // Force listbox to call DrawItem by changing the DisplayMember
Items[Items.IndexOf(item)].Content = content;
}));
}
Note that calling PropertyChanged on the Url is not sufficient. The value must actually change. This tells me that the listbox is caching those values. :-(
(Tested with VS2015 REL)

Host TreeView in Windows Forms DataGridView Cells

I want to create a TreeView column for a DataGridView. I have followed the example in here by extending the TreeView as below.
public class TreeViewEditingControl : TreeView, IDataGridViewEditingControl
public class TreeViewCell : DataGridViewComboBoxCell // Not sure whether this should be DataGridViewTextBoxCell
This is my issue. I can see the Treeview in cells, but I don't know how to increase the height of the Cell/TreeView when user click on a cell (as ComboBox expands). Does anyone have any idea on this?
I would spawn a new borderless form with a TreeCtrl Docked inside, I've done this with a CalendarControl and it works well. The user will not know the difference if you set the upper left hand corner of the form to the upper left hand corner of the cell that is being edited. Hope this is what you are looking for.
Edit:
Here is an implementation I did for a File Selection Cell. It has a Browse button that appears in the cell when you click it for editing and it opens a FileOpenDialog. The code is lengthy, but I think you can pick out the parts you need to implement.
public class DataGridViewFileColumn : DataGridViewColumn
{
public DataGridViewFileColumn() : base(new DataGridViewFileCell())
{
BrowseLabel = "...";
SaveFullPath = false;
}
public override DataGridViewCell CellTemplate
{
get
{
return base.CellTemplate;
}
set
{
// Ensure that the cell used for the template is a DataGridViewFileCell.
if (value != null &&
!value.GetType().IsAssignableFrom(typeof(DataGridViewFileCell)))
{
throw new InvalidCastException("Must be a DataGridViewFileCell");
}
base.CellTemplate = value;
}
}
[Description("Label to place on Browse button"),Category("Appearance")]
[DefaultValue("...")]
public string BrowseLabel
{
get;
set;
}
[Description("Save full path name"), Category("Behavior")]
[DefaultValue(true)]
public bool SaveFullPath
{
get;
set;
}
}
public class DataGridViewFileCell : DataGridViewTextBoxCell
{
public DataGridViewFileCell() : base()
{
}
public override void InitializeEditingControl(int rowIndex, object
initialFormattedValue, DataGridViewCellStyle dataGridViewCellStyle)
{
// Set the value of the editing control to the current cell value.
base.InitializeEditingControl(rowIndex, initialFormattedValue,
dataGridViewCellStyle);
FileEditingControl ctl = (FileEditingControl)DataGridView.EditingControl;
// Use the default row value when Value property is null.
if (this.Value == null)
{
ctl.Filename = this.DefaultNewRowValue.ToString();
}
else
{
ctl.Filename = this.Value.ToString();
}
}
public override Type EditType
{
get
{
// Return the type of the editing control that DataGridViewFileCell uses.
return typeof(FileEditingControl);
}
}
public override Type ValueType
{
get
{
// Return the type of the value that DataGridViewFileCell contains.
return typeof(string);
}
}
}
class FileEditingControl : FileTextBox, IDataGridViewEditingControl
{
DataGridView dataGridView;
private bool valueChanged = false;
int rowIndex;
public FileEditingControl()
{
}
#region IDataGridViewEditingControl implementations
public object EditingControlFormattedValue
{
get
{
return Filename;
}
set
{
if (value is String)
{
try
{
Filename = (String)value;
}
catch
{
Filename = value.ToString();
}
}
}
}
public object GetEditingControlFormattedValue(
DataGridViewDataErrorContexts context)
{
return EditingControlFormattedValue;
}
public void ApplyCellStyleToEditingControl(
DataGridViewCellStyle dataGridViewCellStyle)
{
this.Font = dataGridViewCellStyle.Font;
}
public int EditingControlRowIndex
{
get
{
return rowIndex;
}
set
{
rowIndex = value;
}
}
public bool EditingControlWantsInputKey(
Keys key, bool dataGridViewWantsInputKey)
{
switch (key & Keys.KeyCode)
{
case Keys.Left:
case Keys.Up:
case Keys.Down:
case Keys.Right:
case Keys.Home:
case Keys.End:
case Keys.PageDown:
case Keys.PageUp:
return true;
default:
return !dataGridViewWantsInputKey;
}
}
public void PrepareEditingControlForEdit(bool selectAll)
{
}
public bool RepositionEditingControlOnValueChange
{
get
{
return false;
}
}
public DataGridView EditingControlDataGridView
{
get
{
return dataGridView;
}
set
{
dataGridView = value;
}
}
public bool EditingControlValueChanged
{
get
{
return valueChanged;
}
set
{
valueChanged = value;
}
}
public Cursor EditingPanelCursor
{
get
{
return base.Cursor;
}
}
#endregion
protected override void OnValueChanged(FileEventArgs eventargs)
{
// Notify the DataGridView that the contents of the cell
// have changed.
valueChanged = true;
this.EditingControlDataGridView.NotifyCurrentCellDirty(true);
base.OnValueChanged(eventargs);
}
}
public partial class FileTextBox : UserControl
{
#region Constructors
public FileTextBox()
{
InitializeComponent();
Tooltip = new ToolTip();
SaveFullPath = false;
AllowMultipleFiles = false;
BrowseLabel = "...";
}
#endregion Constructors
#region Properties
/// <summary>
/// Tooltip object used to show full path name
/// </summary>
private ToolTip Tooltip;
/// <summary>
/// Return the full path or just the filename?
/// </summary>
[Description("Save Full Path"), Category("Behavior")]
[DefaultValue(false)]
public bool SaveFullPath
{
get;
set;
}
/// <summary>
/// String representing the filename for this control
/// </summary>
public override string Text
{
get
{
return base.Text;
}
set
{
if (base.Text != value)
{
base.Text = value;
Tooltip.SetToolTip(this, base.Text);
Invalidate();
OnValueChanged(new FileEventArgs(base.Text));
}
}
}
[Description("Browse Label"), Category("Appearance")]
[DefaultValue("...")]
public string BrowseLabel
{
get
{
return Browse.Text;
}
set
{
Browse.Text = value;
Browse.Width = TextRenderer.MeasureText(Browse.Text, Browse.Font).Width + 8;
Browse.Location = new Point(this.Width - Browse.Width, Browse.Location.Y);
}
}
[Description("Allow Multiple Files"), Category("Behavior")]
[DefaultValue(false)]
public bool AllowMultipleFiles
{
get;
set;
}
/// <summary>
/// Selected filename (same as Text property)
/// </summary>
[Description("Filename"), Category("Data")]
public string Filename
{
get { return Text; }
set { Text = value; }
}
#endregion Properties
#region Event Handlers
/// <summary>
/// Event raised when
/// </summary>
public event EventHandler ValueChanged;
protected virtual void OnValueChanged(FileEventArgs eventargs)
{
eventargs.Filename = Filename;
if (this.ValueChanged != null)
this.ValueChanged(this, eventargs);
}
private void Browse_Click(object sender, EventArgs e)
{
OpenFileDialog dlg = new OpenFileDialog();
dlg.FileName = Text;
dlg.Multiselect = AllowMultipleFiles;
if (dlg.ShowDialog() == DialogResult.OK)
{
if (SaveFullPath)
Text = dlg.FileName;
else
Text = dlg.SafeFileName;
}
}
protected override void OnPaint(PaintEventArgs e)
{
// Draw the client window
Rectangle r = new Rectangle(new Point(0, 0), new Size(Size.Width-1, Size.Height-1));
Graphics g = e.Graphics;
g.FillRectangle(new SolidBrush(SystemColors.Window), r);
g.DrawRectangle(new Pen(VisualStyleInformation.TextControlBorder), r);
r.Y += Margin.Top;
r.Width -= Browse.Width;
// Fill with Text
TextRenderer.DrawText(g, Text, Font, r, ForeColor, TextFormatFlags.PathEllipsis);
base.OnPaint(e);
}
private void FileTextBox_DragDrop(object sender, DragEventArgs e)
{
DataObject data = (DataObject)e.Data;
StringCollection filenames = data.GetFileDropList();
if ( filenames.Count == 1)
Text = filenames[0];
}
private void FileTextBox_DragEnter(object sender, DragEventArgs e)
{
DataObject data = (DataObject)e.Data;
StringCollection filenames = data.GetFileDropList();
if (/*!AllowMultipleFiles &&*/ filenames.Count == 1)
e.Effect = DragDropEffects.Link;
}
#endregion Event Handlers
}
public class FileEventArgs : EventArgs
{
public FileEventArgs(string Text)
{
Filename = Text;
}
/// <summary>
/// Name of the file in the control
/// </summary>
public String Filename { get; set; }
}

Categories