All I need is a field that can be renamed by users as they wish by just clicking on it, I am using a label as the control here, when ever user click on it user could be able to enter text in the label and when he click outside of the label that enter text would be saved as the label text.
This is a quick solution:
private void label1_Click(object sender, EventArgs e)
{
TextBox tb = null;
if (label1.Controls.Count > 0) // do we already have created our TextBox?
{
tb = ((TextBox)label1.Controls[0]); // yes. set reference.
// is it already visible? we got clicked from outside, so we hide it:
if (tb.Visible) { label1.Text = tb.Text; tb.Hide(); return; };
}
else if (sender == null) return; // clicked from outside: do nothing
else // we need to create the textbox
{
tb = new TextBox();
tb.Parent = label1; // add it to the label's Controls collection
tb.Size = label1.Size; // size it
// set the event that ends editing when focus goes elsewhere:
tb.LostFocus += (ss, ee) => { label1.Text = tb.Text; tb.Hide(); };
}
tb.Text = label1.Text; // get current text
tb.Show(); // display the textbox in place
}
It embeds a TextBox into the Label. Style the Label to be big enough for the expected user entry!
It expects no other controls to be embedded there.
If you need it more than once consider creating a custom editable label from this code!
Note that to work you need to click at a spot where focus can go, not just the empty space around. To remedy that you could code the Click event for the surrounding space, maybe like this:
private void unClickLabel(object sender, EventArgs e) {label1_Click(null, null);}
In the form constructor add this to all the 'outside' controls that won't take focus, like the Form or a TabPage or a PicureBox or a Panel:
public Form1()
{
InitializeComponent();
this.Click += unClickLabel;
tabPage1.Click += unClickLabel;
pictureBox1.Click += unClickLabel;
..
}
Note that the new Label text will not persist program runs! To allow that you need to store it in some outside user settings and load them at startup..
You should create your own UserControl which contains one label and one textbox. Implement its functionality like you want.
I have created a sample usercontrol to give you an idea about it...
Update:
Follow these steps to use this custom control.
Right click on your project and click 'Add-> UserControl'
Name it 'EditableLabelControl' and click Add.
Go to 'EditableLabelControl.Designer.cs' and replace the partial class Code1 below.
Then go to 'EditableLabelControl.cs' and replace second partial class by Code2 below.
Build your solution.
You should be able to add EditableLabelControl to your form(it will be shown in toolbox)
Code1
partial class EditableLabelControl
{
/// <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.label1 = new System.Windows.Forms.Label();
this.textBox1 = new System.Windows.Forms.TextBox();
this.SuspendLayout();
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(0, 0);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(35, 13);
this.label1.TabIndex = 0;
this.label1.Text = "label1";
this.label1.MouseClick += new System.Windows.Forms.MouseEventHandler(this.label1_MouseClick);
//
// textBox1
//
this.textBox1.Location = new System.Drawing.Point(0, 0);
this.textBox1.Name = "textBox1";
this.textBox1.TabIndex = 1;
this.textBox1.Visible = false;
this.textBox1.Leave += new System.EventHandler(this.textBox1_Leave);
//
// EditableLabelControl
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.AutoSize = true;
this.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
this.Controls.Add(this.textBox1);
this.Controls.Add(this.label1);
this.Name = "EditableLabelControl";
this.Size = new System.Drawing.Size(103, 23);
this.ResumeLayout(false);
this.PerformLayout();
}
private System.Windows.Forms.Label label1;
private System.Windows.Forms.TextBox textBox1;
#endregion
}
Code2
public partial class EditableLabelControl : UserControl
{
public EditableLabelControl()
{
InitializeComponent();
}
private void label1_MouseClick(object sender, MouseEventArgs e)
{
textBox1.Visible = true;
textBox1.BringToFront();
textBox1.Focus();
}
private void textBox1_Leave(object sender, EventArgs e)
{
label1.Text = textBox1.Text;
textBox1.Visible = false;
textBox1.SendToBack();
}
}
Just add this EditableLabelControl to your form and it should work.
Try using TextBox instead of Label.
Use a TextBox control and set the ReadOnly property to true, if you would like to have a non-changeable text field. However, keep in mind, the TextBox will not resize automatically and does not support transparency.
You can change the property BackColor to Control and the BorderStyle to none, this will appear like a label.
With more effort you can create a UserControl and switch between the Lable and TextBox.
I recommend using textBox instead.
Replace the label with textBox.
//Set properties. Make the textBox looks like label
yourTextBox.BackColor = SystemColors.Control;
yourTextBox.BorderStyle = System.Windows.Forms.BorderStyle.None;
//Allow user to change text by double clicking it
yourTextBox.DoubleClick += new EventHandler(this.doubleClick);
//Do stuff when user double click it. Double click again to end editing.
public void doubleClick(object sender, EventArgs e)
{
TextBox temp = (TextBox)sender;
temp.ReadOnly = !temp.ReadOnly;
temp.DeselectAll();
if (temp.ReadOnly)
{
//Make the textbox lose focus
this.ActiveControl = null;
}
}
Related
I want to know if a button is pressed in C# from another class. I have my standard class Form and another class Device. Now I want to access the button from InitializeComponent in Form in my class Device. Does anyone know a good way to do this?
EDIT: If I press on the btnInitialise I want to show a messagebox (with text "test") to start with. I want to use this button in the class Device. I don't rely know how I can reference the button btnInitialise that is automatically made in my form to the class Device.
public class Form1 : System.Windows.Forms.Form
{
#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.tabControl1 = new System.Windows.Forms.TabControl();
this.tabPage1 = new System.Windows.Forms.TabPage();
this.btnInitialise = new System.Windows.Forms.Button();
this.cmbdevice = new System.Windows.Forms.ComboBox();
this.tabControl1.SuspendLayout();
this.tabPage1.SuspendLayout();
this.SuspendLayout();
//
// tabControl1
//
this.tabControl1.Controls.Add(this.tabPage1);
this.tabControl1.Controls.Add(this.tabPage2);
this.tabControl1.Location = new System.Drawing.Point(42, 41);
this.tabControl1.Name = "tabControl1";
this.tabControl1.SelectedIndex = 0;
this.tabControl1.Size = new System.Drawing.Size(645, 414);
this.tabControl1.TabIndex = 0;
//
// tabPage1
//
this.tabPage1.Controls.Add(this.grpDevice);
this.tabPage1.Location = new System.Drawing.Point(4, 22);
this.tabPage1.Name = "tabPage1";
this.tabPage1.Padding = new System.Windows.Forms.Padding(3);
this.tabPage1.Size = new System.Drawing.Size(637, 388);
this.tabPage1.TabIndex = 0;
this.tabPage1.Text = "tabPage1";
this.tabPage1.UseVisualStyleBackColor = true;
//
// btnInitialise
//
this.btnInitialise.Location = new System.Drawing.Point(351, 16);
this.btnInitialise.Name = "btnInitialise";
this.btnInitialise.Size = new System.Drawing.Size(96, 25);
this.btnInitialise.TabIndex = 21;
this.btnInitialise.Text = "Initialize";
this.btnInitialise.Click += new System.EventHandler(this.btnInitialise_Click);
//
// Form1
//
this.ClientSize = new System.Drawing.Size(1005, 532);
this.Controls.Add(this.tabControl1);
this.Name = "Form1";
this.tabControl1.ResumeLayout(false);
this.tabPage1.ResumeLayout(false);
this.grpDevice.ResumeLayout(false);
this.grpDevice.PerformLayout();
this.ResumeLayout(false);
}
private TabControl tabControl1;
private TabPage tabPage1;
private Button btnInitialise;
#endregion "Windows Form Designer generated code"
#region "Global variables"
// OpenLayers fields
////Encapsulates a DT-open layers deviceand manages and distributes subsystems for the device
private Device device = null;
#endregion "Global variables"
//Automatically to initialize components of form
public Form1()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();
//Set the culture to en-US for decimal point instead of decimal comma in results
CultureInfo english = new CultureInfo("en-US");
CultureInfo.DefaultThreadCurrentCulture = english;
}
/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (device != null)
{
device.Dispose();
}
}
base.Dispose(disposing);
}
/// <summary>
/// The main entry point for the application.
/// </summary>
///
//Run application, show error message if appl doesnt run
[STAThread]
private static void Main()
{
try
{
Application.Run(new Form1());
}
catch (Exception ex)
{
MessageBox.Show(ex.Message.ToString(), "Error");
}
}
}
public class Device
{
//When clicking on the initialize button show messagebox
private void btnInitialise_Click(object sender, EventArgs e)
{
MessageBox.Show("test");
}
}
}
You have to expose the button to other classes. One way to do so would be using public property. e.g.
public Button MyButton
{
get { return button1; }
}
then from the Form class you can use it as
Device d = new Device();
Button b = d.MyButton;
Note: This example is based on what you have asked in the post. However, it is not clear what are you actually going to achieve by getting a button like this! If you add a code sample and more info about the application you are developing we can help you better.
Your Form constructor should look like this:
public Form1()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();
//Set the culture to en-US for decimal point instead of decimal comma in results
CultureInfo english = new CultureInfo("en-US");
CultureInfo.DefaultThreadCurrentCulture = english;
device = new Device();
this.btnInitialise.Click += new System.EventHandler(device.btnInitialise_Click);
}
Now in Device you should make the eventhandler public, like this:
public class Device
{
//When clicking on the initialize button show messagebox
public void btnInitialise_Click(object sender, EventArgs e)
{
MessageBox.Show("test");
}
}
Now the Device's btnInitialise_Click method will be triggered when you click the button.
Edit: Fixed typo in Device, try Again.
I have a ComboBox. Its autocomplete mode is set to "SuggestAppend" and the autocomlete source is set to "ListItems". If I try to update the list of items of that combo box in the combo box's Enter event handler, I get the following unexpected behavior.
When the combobox is NOT focused and I focus it by clicking the arrow next to the combo box, the list of items drops and collapes back instantly. Subsequent click on the arrow drop down the list fine because the combo box is already focused.
I suspect that's because when I update the item list inside the Enter event handler, another event is fired (item list changed ?) for the autocomplete to handle its magic, which triggers the combo box to collapse.
What can I do about this? Do note that it is somewhat important for me to update the list of items in the combobox only when I know that that combobox is going to be used soon (hence the enter event handler).
Here's a minimal compilable example (the button is there so that the combobox isn't initially focused):
Form1 Designer:
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.button1 = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// comboBox1
//
this.comboBox1.AutoCompleteMode = System.Windows.Forms.AutoCompleteMode.SuggestAppend;
this.comboBox1.AutoCompleteSource = System.Windows.Forms.AutoCompleteSource.ListItems;
this.comboBox1.FormattingEnabled = true;
this.comboBox1.Location = new System.Drawing.Point(50, 83);
this.comboBox1.Name = "comboBox1";
this.comboBox1.Size = new System.Drawing.Size(190, 24);
this.comboBox1.TabIndex = 1;
this.comboBox1.Enter += new System.EventHandler(this.comboBox1_Enter);
//
// button1
//
this.button1.Location = new System.Drawing.Point(165, 35);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(75, 23);
this.button1.TabIndex = 0;
this.button1.Text = "button1";
this.button1.UseVisualStyleBackColor = true;
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(282, 255);
this.Controls.Add(this.button1);
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.Button button1;
}
Form1.cs:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void comboBox1_Enter(object sender, EventArgs e)
{
string[] items = new string[] { "A", "B", "C" };
comboBox1.Items.Clear();
comboBox1.Items.AddRange(items);
}
}
For your scenario, it's simply a matter of enclosing the ComboBox.Items modification with BeginUpdate / EndUpdate calls:
private void comboBox1_Enter(object sender, EventArgs e)
{
string[] items = new string[] { "A", "B", "C" };
comboBox1.BeginUpdate();
comboBox1.Items.Clear();
comboBox1.Items.AddRange(items);
comboBox1.EndUpdate();
}
It prevents immediate reacting on Items.Clear call which was the cause of the issue.
I've got a really weird problem with ToolStripPanel.Join and I've been searching on Google and SO for some clue as to what I'm doing wrong but I can't figure it out. Basically, when I use ToolStripPanel.Join, the first ToolStrip that I add doesn't appear at all in the ToolStripPanel but all other ToolStrips that I join will appear. Before I get too far into the details, let me first say that I'm using C# and VS 2010 and .NET 4 and, just for some context, I'm trying to use a ToolStripPanel on a user control which is inside of a custom dll that we made so that we could reuse these user controls in other forms.
I was previously using a ToolStripContainer but I decided to switch out to use a ToolStripPanel since we only really needed the top panel of the ToolStripContainer; I didn't see the point of using a ToolStripContainer. Since I couldn't find a ToolStripPanel control in the Toolbox, I decided to add it myself in the Designer.cs file. Here's how I did it:
private ToolStripPanel tsPanel;
<--Other code here-->
private void InitializeComponent()
{
this.tsPanel = new System.Windows.Forms.ToolStripPanel();
<--Other code here-->
//
// tsPanel
//
this.tsPanel.Dock = System.Windows.Forms.DockStyle.Top;
this.tsPanel.Location = new System.Drawing.Point(0, 0);
this.tsPanel.Margin = new System.Windows.Forms.Padding(3);
this.tsPanel.Name = "tsPanel";
this.tsPanel.Orientation = System.Windows.Forms.Orientation.Horizontal;
this.tsPanel.RowMargin = new System.Windows.Forms.Padding(3, 0, 0, 0);
this.tsPanel.Size = new System.Drawing.Size(1000, 0);
<--Other code here-->
//
// MFDesigner
//
this.BackColor = System.Drawing.Color.Gainsboro;
<--Add other controls to UC Controls collection-->
this.Controls.Add(this.tsPanel);
this.ForeColor = System.Drawing.Color.Black;
this.Name = "MFDesigner";
this.Size = new System.Drawing.Size(1000, 670);
this.Load += new System.EventHandler(this.MultiFormatDesignerControl_Load);
this.Resize += new System.EventHandler(this.MFDesigner_Resize);
this.pnlToolbox.ResumeLayout(false);
this.pnlProperties.ResumeLayout(false);
this.ResumeLayout(false);
this.PerformLayout();
}
Then, in the user control's constructor, I have:
public MFDesigner()
{
InitializeComponent();
<--Other code here-->
ToolStripButton[] openSaveButtonArr = new ToolStripButton[]{
//The createToolStripButton method creates toolstrip buttons using some simple
//parameters.
createToolStripButton("Open", Images.CmdOpen, new EventHandler(this.OnOpen), "Open saved file"),
createToolStripButton("Save", Images.CmdSave, new EventHandler(this.OnSaveToDisk), "Save to disk")
};
ToolStrip openSaveToolStrip = new ToolStrip(openSaveButtonArr);
tspanel.Join(openSaveToolStrip);
<--Other code here-->
}
Since we're creating tool strips and adding them to the toolstrippanel in code, I can't see what it looks like in the designer for the user control. So, I build the dll and I go over to another form in a separate project that consumes the user control from the dll and when the form opens, there is no toolstrip; it simply doesn't appear. Here's the weird thing, though. If I add just one more toolstrip to the panel, the second toolstrip will appear:
public MFDesigner()
{
InitializeComponent();
<--Other code here-->
ToolStripButton[] openSaveButtonArr = new ToolStripButton[]{
//The createToolStripButton method creates toolstrip buttons using some simple
//parameters.
createToolStripButton("Open", Images.CmdOpen, new EventHandler(this.OnOpen), "Open saved file"),
createToolStripButton("Save", Images.CmdSave, new EventHandler(this.OnSaveToDisk), "Save to disk")
};
ToolStrip openSaveToolStrip = new ToolStrip(openSaveButtonArr);
tspanel.Join(openSaveToolStrip, 1);
ToolStripButton[] openSaveButtonArr2 = new ToolStripButton[]{
createToolStripButton("Open2", Images.CmdOpen, new EventHandler(this.OnOpen), "Open saved rpx file 2"),
createToolStripButton("Save2", Images.CmdSave, new EventHandler(this.OnSaveToDisk), "Save to disk 2")
};
ToolStrip openSaveToolStrip2 = new ToolStrip(openSaveButtonArr2);
tsPanel.Join(openSaveToolStrip2, 1);
<--Other code here-->
}
In the code above, the first toolstrip that I create will not appear, but the second one (openSaveToolStrip2) will appear. Incidentally, if I just use the Join overload Join(toolStrip) for both of the toolstrips, nothing appears. Also, if I add toolstrips to other rows, i.e. tsPanel.Join(toolstrip3, 2) or tsPanel.Join(toolstrip4, 3), the toolstrips will appear.
It seems like, for some inexplicable (for me, at least) reason, the first toolstrip that I add never appears but all subsequent toolstrips do. As a workaround, I've been just creating a dummy toolstrip, adding it, then adding all of my real toolstrips. This feels pretty hacky so I'd love to figure out why this is happening. I've tried to follow the documentation on MSDN but I must still be missing something because I can't imagine a bug like this not getting fixed.
Does anybody know what might be going wrong here?
Screenshot of running application with all tool strips inside the tool strip panel
I took your code and removed stray members so that I could compile, or stuff that was not relevant for your issue (these for instance: //private Panel pnlToolbox; //private Panel pnlProperties;). I created the runtime tool strips and joined them to the panel, same as you did. I also provided my own implementation of createToolStripButton since you did not. The third toolstrip uses Image.FromStream and web resources since I lacked the local ones. With or without images I had no malfunction whatsoever ...
Complete example below:
public partial class MFDesigner : Form {
private ToolStripPanel tsPanel;
//private Panel pnlToolbox;
//private Panel pnlProperties;
public MFDesigner ( )
{
InitializeComponent();
//
// first toolstrip
//
ToolStripButton tsbOpen = new ToolStripButton("Open", null, new EventHandler(this.OnOpen), "Open saved file");
ToolStripButton tsbSave = new ToolStripButton("Save", null, new EventHandler(this.OnSaveToDisk), "Save to disk");
ToolStripButton[] openSaveButtonArr = new ToolStripButton[] { tsbOpen, tsbSave };
ToolStrip openSaveToolStrip = new ToolStrip(openSaveButtonArr) { CanOverflow = true };
this.tsPanel.Join(openSaveToolStrip);
//
// second toolstrip
//
ToolStripButton tsbOpen2 = createToolStripButton("Open2", null, new EventHandler(this.OnOpen), "Open saved file");
ToolStripButton tsbSave2 = createToolStripButton("Save2", null, new EventHandler(this.OnSaveToDisk), "Save to disk");
ToolStripButton[] openSaveButtonArr2 = new ToolStripButton[] { tsbOpen2, tsbSave2 };
ToolStrip openSaveToolStrip2 = new ToolStrip(openSaveButtonArr2) { CanOverflow = true };
this.tsPanel.Join(openSaveToolStrip2, 1);
//
// third toolstrip
//
WebClient wc = new WebClient();
byte[] bytes = wc.DownloadData("http://my.crestron.eu/images/icons/ico_folder_open.png");
// You must keep the stream open for the lifetime of the Image.
MemoryStream msOpen = new MemoryStream(bytes);
System.Drawing.Image imgOpen = System.Drawing.Image.FromStream(msOpen);
bytes = wc.DownloadData("http://www.njrussians.com/images/save_ico.png");
MemoryStream msSave = new MemoryStream(bytes);
System.Drawing.Image imgSave = System.Drawing.Image.FromStream(msSave);
ToolStripButton tsbOpen3 = createToolStripButton("Open3", imgOpen, new EventHandler(this.OnOpen), "Open saved file");
ToolStripButton tsbSave3 = createToolStripButton("Save3", imgSave, new EventHandler(this.OnSaveToDisk), "Save to disk");
ToolStripButton[] openSaveButtonArr3 = new ToolStripButton[] { tsbOpen3, tsbSave3 };
ToolStrip openSaveToolStrip3 = new ToolStrip(openSaveButtonArr3) { CanOverflow = true };
this.tsPanel.Join(openSaveToolStrip3, 2);
}
ToolStripButton createToolStripButton (string text, Image i, EventHandler eh, string name)
{
return new ToolStripButton(text, i, eh, name);
}
void MFDesigner_Resize (object sender, System.EventArgs e) { }
void MFDesigner_Load (object sender, System.EventArgs e) { }
void OnOpen (object target, EventArgs e) { }
void OnSaveToDisk (object target, EventArgs e) { }
#region Windows Form Designer generated code
/// <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);
}
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent ( )
{
this.tsPanel = new System.Windows.Forms.ToolStripPanel();
this.SuspendLayout();
//
// tsPanel
//
this.tsPanel.Dock = System.Windows.Forms.DockStyle.Top;
this.tsPanel.Location = new System.Drawing.Point(0, 0);
this.tsPanel.Margin = new System.Windows.Forms.Padding(3);
this.tsPanel.Name = "tsPanel";
this.tsPanel.Orientation = System.Windows.Forms.Orientation.Horizontal;
this.tsPanel.RowMargin = new System.Windows.Forms.Padding(3, 0, 0, 0);
this.tsPanel.Size = new System.Drawing.Size(984, 0);
//
// MFDesigner
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.Color.Gainsboro;
this.ClientSize = new System.Drawing.Size(984, 632);
this.Controls.Add(this.tsPanel);
this.ForeColor = System.Drawing.Color.Black;
this.Name = "MFDesigner";
this.Text = "MFDesigner";
this.Load += new System.EventHandler(this.MFDesigner_Load);
this.Resize += new System.EventHandler(this.MFDesigner_Resize);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
}
I have finally tracked down a bug that I've been working on for the entire weekend, however I don't see a way to really solve it - in a clean way, that is.
The situation is that I have a DGV control that is bound to a List of business objects. One of the columns is NOT bound to the DataSource and I have wired up the CellParsing and CellFormatting events to handle the persistence of the data for this cell. I've basically circumvented the .net databinding and implemented my own poor version. This was not intentional, it's REALLY old code that had complicated parsing and formatting requirements and I incorrectly implemented a solution based on an unbound column. I now know the correct way to handle this and have since fixed my code, however I'd still like to know how I could have solved the bug another way.
I handle the RowValidating event and do one final validation on the row as a whole to ensure everything is cool. Of course if there is an issue I Cancel the validation and the row is not committed. This all works fine when the user is editing and adding rows through UI interaction, but creates a problem when the DataSource is set. The issue appears to be that CellFormatting is not called when the DGV updates it's internal list and builds the rows, or at least it's not called before the validating event is fired. This results in the RowValidating handler pulling a null value from the Unbound column (because CellFormatting hasn't been called and set the value yet).
I was refreshing my knowledge on the DGV and thought that handling the CellValueNeeded event may be the ticket but setting DataGridViewCellValueEventArgs.Value didn't trigger the CellFormatting event like I hoped it would.
I've been thinking about how to handle this situation and the only thing I have come up with is to detect when the validation is triggered from a UI event rather than the initial binding or bound list change. Not only is this a hacky solution, but don't see how it could be done.
I've created a complete example application that will illustrate the problem. I'd be really curious to see how some of you would solve a problem like this. It's likely that there is major design smell here.
using System;
using System.Collections.Generic;
using System.Windows.Forms;
public class Form1 : Form
{
private List<DomainModel> _sampleData;
public Form1()
{
InitializeComponent();
_sampleData = new List<DomainModel>();
_sampleData.Add(new DomainModel("Widget A"));
_sampleData.Add(new DomainModel("Widget B"));
}
private void button1_Click(object sender, EventArgs e)
{
Console.WriteLine("Setting DataSource");
domainModelBindingSource.DataSource = _sampleData;
}
private void dataGridView1_CellFormatting(object sender,
DataGridViewCellFormattingEventArgs e)
{
Console.WriteLine("CellFormatting fired for {0},{1}",
e.RowIndex, e.ColumnIndex);
if (e.ColumnIndex != 0 && !dataGridView1.Rows[e.RowIndex].IsNewRow)
{
var model = domainModelBindingSource[e.RowIndex] as DomainModel;
e.Value = model.Name;
e.FormattingApplied = true;
}
}
private void dataGridView1_CellParsing(object sender, DataGridViewCellParsingEventArgs e)
{
if (e.ColumnIndex == 1)
{
e.Value = e.Value.ToString();
e.ParsingApplied = true;
}
}
private void dataGridView1_RowValidating(object sender, DataGridViewCellCancelEventArgs e)
{
if (dataGridView1.Rows[e.RowIndex].IsNewRow)
return;
object value = dataGridView1[1, e.RowIndex].Value;
if (value == null || String.IsNullOrEmpty(value.ToString()))
e.Cancel = true;
}
#region Designer stuff
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
components.Dispose();
base.Dispose(disposing);
}
#region Windows Form Designer generated code
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.dataGridView1 = new System.Windows.Forms.DataGridView();
this.nameDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column1 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.domainModelBindingSource = new System.Windows.Forms.BindingSource(this.components);
this.button1 = new System.Windows.Forms.Button();
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.domainModelBindingSource)).BeginInit();
this.SuspendLayout();
//
// dataGridView1
//
this.dataGridView1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.dataGridView1.AutoGenerateColumns = false;
this.dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dataGridView1.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.nameDataGridViewTextBoxColumn,
this.Column1});
this.dataGridView1.DataSource = this.domainModelBindingSource;
this.dataGridView1.Location = new System.Drawing.Point(12, 41);
this.dataGridView1.Name = "dataGridView1";
this.dataGridView1.Size = new System.Drawing.Size(437, 161);
this.dataGridView1.TabIndex = 0;
this.dataGridView1.CellFormatting += new System.Windows.Forms.DataGridViewCellFormattingEventHandler(this.dataGridView1_CellFormatting);
this.dataGridView1.CellParsing += new System.Windows.Forms.DataGridViewCellParsingEventHandler(this.dataGridView1_CellParsing);
this.dataGridView1.RowValidating += new System.Windows.Forms.DataGridViewCellCancelEventHandler(this.dataGridView1_RowValidating);
//
// nameDataGridViewTextBoxColumn
//
this.nameDataGridViewTextBoxColumn.DataPropertyName = "Name";
this.nameDataGridViewTextBoxColumn.HeaderText = "Name";
this.nameDataGridViewTextBoxColumn.Name = "nameDataGridViewTextBoxColumn";
//
// Column1
//
this.Column1.HeaderText = "Data (unbound)";
this.Column1.Name = "Column1";
//
// domainModelBindingSource
//
this.domainModelBindingSource.DataSource = typeof(DomainModel);
//
// button1
//
this.button1.Location = new System.Drawing.Point(12, 12);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(168, 23);
this.button1.TabIndex = 1;
this.button1.Text = "Update Data Source";
this.button1.UseVisualStyleBackColor = true;
this.button1.Click += new System.EventHandler(this.button1_Click);
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(461, 214);
this.Controls.Add(this.button1);
this.Controls.Add(this.dataGridView1);
this.Name = "Form1";
this.Text = "Form1";
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.domainModelBindingSource)).EndInit();
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.DataGridView dataGridView1;
private System.Windows.Forms.BindingSource domainModelBindingSource;
private System.Windows.Forms.Button button1;
private System.Windows.Forms.DataGridViewTextBoxColumn nameDataGridViewTextBoxColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn Column1;
#endregion
}
internal sealed class DomainModel
{
public DomainModel() { }
public DomainModel(string name)
{
this.Name = name;
}
public string Name { get; set; }
}
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
I had mentioned that I already "fixed the problem" which wasn't completely accurate. I fixed it in the test application, effectively validating the solution I was planning on using, but hadn't actually implemented it. Once I tried to I realized why I did things the way I did originally: The data that is used in the unbound column is a read-only collection of objects and in order to set them I need to call a separate method on the object. Best practices and all...
Well, read only properties of course can't be edited in the DGV so I was dead in the water. This made me go BACK to my original design and while researching I stumbled on this column/blog by Brian Noyes.
He mentions the event RowsAdded and that's all I needed!
Here is the solution for my problem:
private void dataGridView1_RowsAdded(object sender,
DataGridViewRowsAddedEventArgs e)
{
if (dataGridView1.Rows[e.RowIndex].IsNewRow)
{
return;
}
dataGridView1[1, e.RowIndex].Value =
(dataGridView1.Rows[e.RowIndex].DataBoundItem as DomainModel).Name;
}
I like this solution because it's placing the data on the Cell the same way it would if it were bound, it then hands off formatting responsibility to CellFormatting and I can do my work. Likewise I can parse the data and construct the required objects to pass to my bound objects "SetListData" method.
All good!
I have a list of words. The list contains about 100-200 text strings (it's names of metro stations actually).
I want to make an auto-complete textbox. For an example, user press 'N' letter, then an (ending of) appropriate option appear (only one option). The ending must be selected.
How to do that?
PS1: I guess, there is a textbox control with a Property something like this:
List<string> AppropriateOptions{/* ... */}
PS2: sorry for my english. If you didn't understand -> ask me and I will try to explain!
Just in case #leniel's link goes down, here's some code that does the trick:
AutoCompleteStringCollection allowedTypes = new AutoCompleteStringCollection();
allowedTypes.AddRange(yourArrayOfSuggestions);
txtType.AutoCompleteCustomSource = allowedTypes;
txtType.AutoCompleteMode = AutoCompleteMode.Suggest;
txtType.AutoCompleteSource = AutoCompleteSource.CustomSource;
Use a ComboBox instead of a TextBox. The following example will autocomplete, matching any piece of the text, not just the starting letters.
This should be a complete form, just add your own data source, and data source column names. :-)
using System;
using System.Data;
using System.Windows.Forms;
public partial class frmTestAutocomplete : Form
{
private DataTable maoCompleteList;
private const string MC_DISPLAY_COL = "name";
private const string MC_ID_COL = "id";
public frmTestAutocomplete()
{
InitializeComponent();
}
private void frmTestAutocomplete_Load(object sender, EventArgs e)
{
maoCompleteList = oData.PurificationRuns;
maoCompleteList.CaseSensitive = false; //turn off case sensitivity for searching
testCombo.DisplayMember = MC_DISPLAY_COL;
testCombo.ValueMember = MC_ID_COL;
testCombo.DataSource = GetDataTableFromDatabase();
testCombo.SelectedIndexChanged += testCombo_SelectedIndexChanged;
testCombo.KeyUp += testCombo_KeyUp;
}
private void testCombo_KeyUp(object sender, KeyEventArgs e)
{
//use keyUp event, as text changed traps too many other evengts.
ComboBox oBox = (ComboBox)sender;
string sBoxText = oBox.Text;
DataRow[] oFilteredRows = maoCompleteList.Select(MC_DISPLAY_COL + " Like '%" + sBoxText + "%'");
DataTable oFilteredDT = oFilteredRows.Length > 0
? oFilteredRows.CopyToDataTable()
: maoCompleteList;
//NOW THAT WE HAVE OUR FILTERED LIST, WE NEED TO RE-BIND IT WIHOUT CHANGING THE TEXT IN THE ComboBox.
//1).UNREGISTER THE SELECTED EVENT BEFORE RE-BINDING, b/c IT TRIGGERS ON BIND.
testCombo.SelectedIndexChanged -= testCombo_SelectedIndexChanged; //don't select on typing.
oBox.DataSource = oFilteredDT; //2).rebind to filtered list.
testCombo.SelectedIndexChanged += testCombo_SelectedIndexChanged;
//3).show the user the new filtered list.
oBox.DroppedDown = true; //this will overwrite the text in the ComboBox, so 4&5 put it back.
//4).binding data source erases text, so now we need to put the user's text back,
oBox.Text = sBoxText;
oBox.SelectionStart = sBoxText.Length; //5). need to put the user's cursor back where it was.
}
private void testCombo_SelectedIndexChanged(object sender, EventArgs e)
{
ComboBox oBox = (ComboBox)sender;
if (oBox.SelectedValue != null)
{
MessageBox.Show(string.Format(#"Item #{0} was selected.", oBox.SelectedValue));
}
}
}
//=====================================================================================================
// code from frmTestAutocomplete.Designer.cs
//=====================================================================================================
partial class frmTestAutocomplete
{
/// <summary>
/// Required designer variable.
/// </summary>
private readonly 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.testCombo = new System.Windows.Forms.ComboBox();
this.SuspendLayout();
//
// testCombo
//
this.testCombo.FormattingEnabled = true;
this.testCombo.Location = new System.Drawing.Point(27, 51);
this.testCombo.Name = "testCombo";
this.testCombo.Size = new System.Drawing.Size(224, 21);
this.testCombo.TabIndex = 0;
//
// frmTestAutocomplete
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(292, 273);
this.Controls.Add(this.testCombo);
this.Name = "frmTestAutocomplete";
this.Text = "frmTestAutocomplete";
this.Load += new System.EventHandler(this.frmTestAutocomplete_Load);
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.ComboBox testCombo;
}
The answer link by Leniel was in vb.net, thanks Joel for your entry. Supplying my code to make it more explicit:
private void InitializeTextBox()
{
AutoCompleteStringCollection allowedStatorTypes = new AutoCompleteStringCollection();
var allstatortypes = StatorTypeDAL.LoadList<List<StatorType>>().OrderBy(x => x.Name).Select(x => x.Name).Distinct().ToList();
if (allstatortypes != null && allstatortypes.Count > 0)
{
foreach (string item in allstatortypes)
{
allowedStatorTypes.Add(item);
}
}
txtStatorTypes.AutoCompleteMode = AutoCompleteMode.Suggest;
txtStatorTypes.AutoCompleteSource = AutoCompleteSource.CustomSource;
txtStatorTypes.AutoCompleteCustomSource = allowedStatorTypes;
}
Use combo box, sets its datasource or give hard coded entries but set the following properties:
AutoCompleteMode = Suggest;
AutoCompleteSource = ListItems;
You want to set the TextBox.AutoCompleteSource to CustomSource and then add all of your strings to its AutoCompleteCustomSource property, which is a StringCollection. Then you should be good to go.
I want to add that the standard autocomplete for TextBox does only work from the beginning of your strings, so if you hit N only strings starting with N will be found. If you want to have something better, you have to use some different control or implement the behavior for yourself (i.e. react on TextChanged Event with some timer to delay execution, than filter your tokenlist searching with IndexOf(inputString) and then set your AutoCompleteSource to the filtered list.