This question already has answers here:
When is a C# value/object copied and when is its reference copied?
(3 answers)
Closed 8 years ago.
I am working on a Microsoft Visual C# Form Application that is to be used to create all the data that would go into RPG game. I have designed a structure and encased it all into a class so I can easily read and write to/from an XML file.
On my main form I have a "public static"/"Global" variable that all of my sub forms can copy information from... manipulate what it needs to... and send that information back to it.
For example. I want multiple types of currencies. The form that deals with currencies copies only the currency data from the global variable and has free reign to manipulate only THAT copy. Only when the user clicks the "Apply" or "Accept" button should the global variable be updated to reflect those changes. If the "Cancel" button is clicked it should just close the form and the copied data should get tossed to the winds when the form is disposed.
Unfortunately this is not the case. Whenever I change the data of the copy its changes seem to reflect on the global variable as well. Is there a concept here I am missing here and don't understand. Somebody please explain. I've checked over my code and there are only those two points where the data should be updated.
Code from "Main" Form
public partial class frmMain : Form
{
public static RPGDataCollection DATA = new RPGDataCollection();
public static string FILE = "";
public frmMain ()
{
InitializeComponent();
FillDefaultData();
}
/// <summary>
/// Sets item info status text.
/// </summary>
/// <param name="text">Text to be displayed.</param>
private void SetItemInfo (string text)
{
lblItemInfo.Text = text;
}
Currency Form Code
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using RPGData.ObjectTypes.DataGroups;
namespace RPGData.EntryForms
{
public partial class frmCurrencies : Form
{
#region Fields
private List<Currency> datCurrencies = new List<Currency>();
private string strEntry = "";
private bool blnGSC = false;
private bool blnUpperFlag = false;
private int intIndex = 0;
#endregion
#region Constructor
public frmCurrencies ()
{
InitializeComponent();
}
#endregion
#region Events
private void frmCurrencies_Load (object sender, EventArgs e)
{
datCurrencies = frmMain.DATA.Currencies;
DisplayData();
}
private void btnReplace_Click (object sender, EventArgs e)
{
intIndex = lstCurrencies.SelectedIndex;
Currency c = datCurrencies[intIndex];
if (txtEntry.Text.Trim().Length > 0)
{
SetValues();
c.Name = strEntry;
c.HandlesGSC = blnGSC;
c.Amount = 0;
datCurrencies[intIndex] = c;
}
ResetFields();
DisplayData();
}
private void btnCancel_Click (object sender, EventArgs e)
{
this.Close();
}
private void btnAdd_Click (object sender, EventArgs e)
{
if (txtEntry.Text.Trim().Length > 0)
{
SetValues();
Currency c = new Currency();
c.Name = strEntry;
c.HandlesGSC = blnGSC;
c.Amount = 0;
datCurrencies.Add(c);
}
ResetFields();
DisplayData();
}
private void btnRemove_Click (object sender, EventArgs e)
{
intIndex = lstCurrencies.SelectedIndex;
if (intIndex >= 0)
datCurrencies.RemoveAt(intIndex);
ResetFields();
DisplayData();
}
private void btnApply_Click (object sender, EventArgs e)
{
frmMain.DATA.Currencies = datCurrencies;
}
private void btnAccept_Click (object sender, EventArgs e)
{
frmMain.DATA.Currencies = datCurrencies;
this.Close();
}
private void lstCurrencies_SelectedIndexChanged (object sender, EventArgs e)
{
intIndex = lstCurrencies.SelectedIndex;
Currency c = datCurrencies[intIndex];
txtEntry.Text = c.Name;
chkGSC.Checked = c.HandlesGSC;
}
#endregion
#region Methods
private void DisplayData ()
{
lstCurrencies.Items.Clear();
for (int i = 0; i < datCurrencies.Count; i++)
{
string gsc = "";
if (datCurrencies[i].HandlesGSC)
gsc = "*";
else
gsc = " ";
lstCurrencies.Items.Add("[ " + gsc + " ] " + datCurrencies[i].Name);
}
}
private void ResetFields ()
{
strEntry = "";
blnGSC = false;
txtEntry.Text = strEntry;
chkGSC.Checked = blnGSC;
txtEntry.Focus();
}
private void SetValues ()
{
string entry = ToAllUpper(txtEntry.Text);
strEntry = entry;
blnGSC = chkGSC.Checked;
}
private string ToAllUpper (string str)
{
string entry = "";
for (int i = 0; i < str.Length; i++)
{
string c = "";
if (i == 0)
c = str.Substring(0, 1).ToUpper();
else if (str.Substring(i, 1) == " ")
{
c = " ";
blnUpperFlag = true;
}
else if (blnUpperFlag)
{
c = str.Substring(i, 1).ToUpper();
blnUpperFlag = false;
}
else
c = str.Substring(i, 1);
entry += c;
}
return entry;
}
#endregion
}
}
Any help you can toss me and help me understand what might be happening would be great (or you see a bug or mistake I don't).
Thanks!
This line of code datCurrencies = frmMain.DATA.Currencies is actually creates another one reference to frmMain.DATA.Currencies and doesn't deep copying it.
So all changes you made - is actually made on original object.
You have to clone it (create deep copy), not just create additional reference.
For example, if your Currency is struct (value-type), following will be sufficient:
datCurrencies = new List<Currency>(frmMain.DATA.Currencies);
But if your Currency is class - you may consider following approach:
create Clone method in your Currency class that will return clone of current object and then populate your datCurrencies like:
datCurrencies = new List<Currency>(frmMain.DATA.Currencies.Count);
foreach(var currency in frmMain.DATA.Currencies)
datCurrencies.Add(currency.Clone());
You copy the reference to a list here:
datCurrencies = frmMain.DATA.Currencies;
Afetr this asignment there is still just one list and datCurrencies points to this one global list.
Instead you need to create a deep copy i.e copy the contents from your global list to your local list.
Related
I am getting an error that my textfile that is used to create and save my dictionary is being used by another process, I have used Process explorer to no result on what could be using my file. Below is my code and the code throwing this error.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO;
namespace meade_9_10
{
public partial class Form1 : Form
{
private Dictionary<string, string> names = new Dictionary<string, string>()
{
};
public Form1()
{
//Make sure Form1 is loaded and ran on program open
InitializeComponent();
this.Load += Form1_Load;
}
private void Form1_Load(object sender, EventArgs e)
{
//Grab myfile.txt and convert to array
StreamReader sr = new StreamReader("myfile.txt");
string line;
while ((line = sr.ReadLine()) != null)
{
string[] arr = line.Split(',');
int i = 0;
//Add array objects to names dictionary
while (i < arr.Length)
{
names[arr[i]] = arr[i + 1];
i += 2;
}
}
}
private void btnAdd_Click(object sender, EventArgs e)
{
//declare variables
string nameAdd;
string emailAdd;
//Put user input into variable
nameAdd = txtNameAdd.Text;
emailAdd = txtEmailAdd.Text;
//Declare new dictionary key pair as user input
names[emailAdd] = nameAdd;
//clear the textbox controls
txtNameAdd.Text = "";
txtEmailAdd.Text = "";
}
private void btnDelete_Click(object sender, EventArgs e)
{
string emailDel;
emailDel = txtEmailDel.Text;
//Remove key pair that is inputted by user
names.Remove(emailDel);
}
private void btnChange_Click(object sender, EventArgs e)
{
//Declare variables
string emailDel;
string nameAdd;
string emailAdd;
//Assign values to variables
emailDel = txtEmailChange.Text;
nameAdd = txtNameNew.Text;
emailAdd = txtEmailNew.Text;
//Delete the user inputted email to change
names.Remove(emailDel);
//Add the new key pair values to dictionary
names.Add(emailAdd, nameAdd);
}
private void btnLookUp_Click(object sender, EventArgs e)
{
//Declare variable
string email = txtEmail.Text;
//If statement to check if dictionary contains key value
if (names.ContainsKey(email))
{
outputName.Text = names[email];
outputEmail.Text = email;
}
}
private void btnExit_Click(object sender, EventArgs e)
{
//writes the names dictioanry to array inside text file
File.WriteAllLines("myfile.txt",
names.Select(x => x.Key + "," + x.Value ).ToArray());
//Closes the program
this.Close();
}
}
}
The part of my code giving me the error
System.IO.IOException: 'The process cannot access the file 'C:\Users\Adrian\Desktop\ALL SCHOOL FILES\Fall 2021\C#\meade_9_10\bin\Debug\myfile.txt' because it is being used by another process.'
is
names.Select(x => x.Key + "," + x.Value ).ToArray());
I just cannot figure out what process is using my text file that is breaking this program, it was working earlier and I haven't made any changes except for removing redundant white space between functions.
Try using the
StreamReader.Close() method after your innermost while loop:
Closes the StreamReader object and the underlying stream, and releases any system resources associated with the reader.
Alternatively, you can use the using statement:
Provides a convenient syntax that ensures the correct use of IDisposable objects.
I am making a Windows Forms App that manages a hotel. It has Client, Room, Occupancy classes. Client and Rooms have an ArrayList that is populated at runtime from a .txt file that is then displayed in a clientListView and a roomDataGridView.
As such, I have this line of code to populate the roomsDGV:
roomsDGV.DataSource = roomsArrayList;
With the roomsDGV, I'm trying to add new Rows by clicking on the roomsDGV, like when it is NOT databound. I am also trying to edit the rows and save it to txt file after editing or as I'm editing. I can post more code as necessary but I'm not sure if showing more code will help at the current moment. In the end, I'm trying for a functionality so that I can highlight a client in the list and click on one of the rows in roomsDGV and assign that clientID to that room or any sort of way like that.
On load, the datagridview is loaded and formatted correctly from the arrayList but I seem to be having this problem of being able to edit the datagridview. It gives me this error when I click on one of the rows:
System.IndexOutOfRangeException: 'Index -1 does not have a value.'
This stems from Application.Run(new HotelManager());
Here is the form:
public partial class HotelManager : Form
{
// VARIABLES
string clientID;
// FILEPATHS
string clientsTxt = "Clients.txt";
string occupanciesTxt = "Occupancies.txt";
string roomsTxt = "Rooms.txt";
string clientsDat = "Clients.dat";
// ARRAYLIST FOR ROOMS and CLIENTS
ArrayList roomsArrayList = new ArrayList();
ArrayList clientsArrayList = new ArrayList();
//STACKS AND QUEUES INIT
// Load occupancies into stack > pop
Stack roomStack = new Stack();
Queue vacancyQueue = new Queue();
// RANDOM for ID
private readonly Random rand = new Random();
public HotelManager()
{
InitializeComponent();
}
private void HotelManager_Load(object sender, EventArgs e)
{
roomsDGV.DataSource = roomsArrayList;
// LOAD clients
// LoadClients();
RefreshClientList();
// LOAD rooms
LoadRooms();
}
private void NewClientButton_Click(object sender, EventArgs e)
{
AddClient();
}
private void checkInButton_Click(object sender, EventArgs e)
{
string clientID = clientList.SelectedItems[0].Text;
string[] text = File.ReadAllLines(occupanciesTxt);
foreach (string s in text)
{
if (s.Contains(clientID))
{
var replace = s;
Console.WriteLine(s);
replace = replace.Replace("false", "true");
}
}
File.WriteAllLines(occupanciesTxt, text);
}
// METHODS
private void AddClient()
{
//COLLECT DATA > CREATE NEW client > SHOW IN **PROGRAM/DataGridView** > add to clients file
// ID GENERATION > CHECKS AGAINST clientsTXT
clientID = rand.Next(0, 999999).ToString();
if (File.ReadLines(clientsTxt).Contains(clientID))
{
clientID = rand.Next(0, 999999).ToString();
}
Client client = new Client(clientID, firstNameBox.Text, lastNameBox.Text);
try
{
if (!string.IsNullOrWhiteSpace(phoneNumBox.Text))
{
client.PhoneNumber = Convert.ToInt64(phoneNumBox.Text);
}
if (!string.IsNullOrWhiteSpace(addressBox.Text))
{
client.Address = addressBox.Text;
}
}
catch (Exception)
{
MessageBox.Show("Please use the correct format!");
throw;
}
clientsArrayList.Add(client);
using (StreamWriter file =
new StreamWriter("Clients.txt", true))
{
file.WriteLine(client.ToString());
}
RefreshClientList();
// TEST CODE // SERIALIZATION TO .DAT
SerializeClientData(client);
}
private void LoadClients()
{
// LOADS arrayList FROM .txt FILE
List<string> clientList = File.ReadAllLines(clientsTxt).ToList();
foreach (var c in clientList)
{
Client client = new Client(c);
clientsArrayList.Add(client);
}
}
private void LoadRooms()
{
List<string> roomsList = File.ReadAllLines(roomsTxt).ToList();
foreach (var r in roomsList)
{
var roomDetails = r.Split('|');
if (r.Contains("BASIC"))
{
BasicRoom basic = new BasicRoom();
basic.RoomNumber = roomDetails[0];
basic.NumberOfBeds = Convert.ToInt32(roomDetails[1]);
basic.Balcony = Convert.ToBoolean(roomDetails[2]);
basic.DownForRepair = Convert.ToBoolean(roomDetails[3]);
basic.Smoking = Convert.ToBoolean(roomDetails[4]);
roomsArrayList.Add(basic);
}
else if (r.Contains("SUITE"))
{
Suite suite = new Suite();
suite.RoomNumber = roomDetails[0];
suite.NumberOfBeds = Convert.ToInt32(roomDetails[1]);
suite.Balcony = Convert.ToBoolean(roomDetails[2]);
suite.DownForRepair = Convert.ToBoolean(roomDetails[3]);
suite.NumberOfRooms = Convert.ToInt32(roomDetails[4]);
roomsArrayList.Add(suite);
}
}
roomStack = new Stack(roomsArrayList);
foreach (var item in roomStack)
{
Console.WriteLine(item);
}
}
private void RoomsDGV_CellContentClick(object sender, DataGridViewCellEventArgs e)
{
}
private void RoomsDGV_CellBeginEdit(object sender, DataGridViewCellCancelEventArgs e)
{
}
}
So far I've looked through all the properties but I can't seem to find the right one. I know I can add/use comboboxes and etc to add a new item into the arrayList instead but I'm trying for datagridview functionality
I expect to edit and add rows to the DGV, but something in the designer is preventing me?
Here is the DGV, and clicking on any of the rows breaks it.
https://imgur.com/a/GG7ZwdV
check this
Insert, Update, Delete with DataGridView Control in C# (Windows Application) | A Rahim Khan's Blog
Insert, Update and Delete Records in a C# DataGridView
Good Luck
enter image description here
namespace Implementer
{
public partial class MainForm : Form
{
#region intit and globals
public MainForm()
{
InitializeComponent();
DevExpress.XtraGrid.Views.Grid.GridView gridView = new DevExpress.XtraGrid.Views.Grid.GridView();
var transactions = new ObservableCollection<Item>();
for (int i = 0; i <= 100; i++)
{
transactions.Add(new Item { Content = "Item " + i });
}
grdTransactions.AllowDrop = true;
grdTransactions.DataSource = transactions;
dgmWf.AddingNewItem += (s, e) => transactions.Remove(e.Item.Tag as Item);
}
Point mouseDownLocation;
GridHitInfo gridHitInfo;
#endregion
#region events
public void Mainform_Load(object sender, EventArgs e)
{
// TODO: This line of code loads data into the 'vA_ERP4_ADMINDataSet.SYSTEM_MODULES' table. You can move, or remove it, as needed.
//Project initiation
//Fill drop down for project list
cmbProject.SelectedIndex = 0;
DataAccess dataAccess = new DataAccess(GlobalFunctions.GetConnectionString());
DataTable dtResult = dataAccess.ExecuteQueryDataSet("select MODULE_CODE, MODULE_DESC from SYSTEM_MODULES where module_is_active=1").Tables[0];
cmbProject.DisplayMember = "MODULE_DESC";
cmbProject.ValueMember = "MODULE_CODE";
cmbProject.DataSource = dtResult;
}
private void cmbProject_SelectedIndexChanged(object sender, EventArgs e)
{
lblCurrentProject.Text = cmbProject.Text;
if (cmbProject.Text != null)
{
DataAccess dataAccess = new DataAccess(GlobalFunctions.GetConnectionString());
DataTable dtTransactions = dataAccess.ExecuteQueryDataSet("select sys_trans_id, sys_trans_desc1 from WF_SYSTEM_TRANS where MODULE_CODE= '" + cmbProject.Text + "'").Tables[0];
grdTransactions.DataSource = dtTransactions;
}
}
private void btnSalesInvoice_Click(object sender, EventArgs e)
{
int sysTransId = 1001;
FillTransactionDetails(sysTransId);
}
#endregion
#region drag drop
private void grdTransactions_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left && CanStartDragDrop(e.Location))
{
StartDragDrop();
}
}
private void grdTransactions_MouseDown(object sender, MouseEventArgs e)
{
gridHitInfo = grdVTransactions.CalcHitInfo(e.Location);
mouseDownLocation = e.Location;
}
private void grdTransactions_MouseLeave(object sender, EventArgs e)
{
if (gridHitInfo != null)
gridHitInfo.View.ResetCursor();
gridHitInfo = null;
}
private bool CanStartDragDrop(Point location)
{
return gridHitInfo.InDataRow && (Math.Abs(location.X - mouseDownLocation.X) > 2 || Math.Abs(location.Y - mouseDownLocation.Y) > 2);
}
public void StartDragDrop()
{
var draggedRow = gridHitInfo.View.GetRow(gridHitInfo.RowHandle) as Item;
var tool = new FactoryItemTool(" ", () => " ", diagram => new DiagramShape(BasicShapes.Rectangle) { Content = draggedRow.Content, Tag = draggedRow }, new System.Windows.Size(150, 100), false);
dgmWf.Commands.Execute(DiagramCommandsBase.StartDragToolCommand, tool, null);
}
#endregion
#region function
private void FillTransactionDetails(int systemTransactionId)
{
//Fill document
//Fill steps
DataAccess dataAccess = new DataAccess(GlobalFunctions.GetConnectionString());
DataTable transactionDetails = dataAccess.ExecuteQueryDataSet("SELECT DOC_TYPE_DESC1 FROM WF_SYSTEM_TRANS_DT WHERE SYS_TRANS_ID=1001 and MODULE_CODE= '" + cmbProject.Text + "'").Tables[0];
transactionDetails.Rows.Add();
grdDocuments.DataSource = transactionDetails;
grdDocuments.Columns["Details"].DisplayIndex = 2;
grdDocuments.Columns["Delete"].DisplayIndex = 2;
DataTable transactionSteps = dataAccess.ExecuteQueryDataSet("select WF_STEP_DESC1 from WF_STEPS where wf_id= 10101 and MODULE_CODE= '" + cmbProject.Text + "'").Tables[0];
transactionSteps.Rows.Add();
grdSteps.DataSource = transactionSteps;
}
#endregion
}
public class Item
{
public string Content { get; set; }
}
}
I don't really know where is the mistake and have been looking at it for the past few days and searching for an answer but no luck so I'd be so happy if you could help me out. It was working without the data fetching. but after calling the data it doesn't work. drag it from the grid view and when it reaches the diagram control it would turn into a rectangle with a tag property of it's ID.. With regards to the connection string.. I created a global function to just call it on the main form.
I have a very simple form that produces a Datagridview with 4 columns. No problem generating the form. Only the last column can be edited. After editing, I want to hit the OK button and join elements of the list in the last column into a string. How do I make that list (current) available inside the the OK button object sender. Sorry for my poor grammar. I'm an obvious newbie. Thanks.
using System;
using System.Collections.Generic;
using System.Windows.Forms;
namespace Avisynth_Script
{
public partial class Form4 : Form
{
public Form4()
{
InitializeComponent();
}
public string current_values = null;
private void Load_Tweaker(object sender, EventArgs e)
{
string[] deshake_parts = EntryPoint.deshakeSettings.Split(',');
List<String> settings = new List<string>(deshake_parts);
string[] default_values = EntryPoint.deshakeDefaultValues.Split(',');
List<String> defaults = new List<string>(default_values);
string[] current_settings = current_values.Split('|');
//current_settings[0] = current_settings[0].Substring(1);
//current_settings[66] = current_settings[66].Substring(0, 6);
List<String> current = new List<string>(current_settings);
dataGridView1.Rows.Clear();
for (int i=0; i < settings.Count; i++)
{
dataGridView1.Rows.Add(i+1 ,settings[i], defaults[i], current[i]);
}
}
private void OK_button_Click(object sender, EventArgs e)
{
EntryPoint.deshaker_param = string.Join('|', current.ToArray());
}
}
}
You have an unbound DataGridView with four columns and want to concatenate the values in the fourth column separated by a "|" in the OK_button_Click method.
You could use a foreach loop to iterate each row in the DataGridView's row collection and pull the value from the fourth column (index=3) to achieve this, or you could use a Linq query to perform the iteration.
Here is a Linq solution.
private void OK_button_Click(object sender, EventArgs e)
{
EntryPoint.deshaker_param = string.Join("|", dataGridView1.Rows.Cast<DataGridViewRow>().Where(row => (!row.IsNewRow)).Select(row =>((row.Cells[3].Value ?? string.Empty).ToString())));
}
It wouldn't be too difficult to move current to be a field-level variable, but that isn't the best option. Now that C# has lambdas in the language, you can basically inline the event handler and remove the OK_button_Click method entirely. This kind of encapsulation is a more robust approach than just making your variables have a higher level of access.
Using a field is like saying that someone in particular needs to write to a file on your computer so you'll just make that file available for everyone to access. You wouldn't do that with your files on your computer so you shouldn't do that in your code.
Inside a lambda you can access the method's local variables. You code can look like this:
List<String> current = new List<string>(current_settings);
OK_button.Click += (s, e2) =>
{
EntryPoint.deshaker_param = string.Join("|", current.ToArray());
};
If you only ever call Load_Tweaker once then your code can be very simple:
private void Load_Tweaker(object sender, EventArgs e)
{
string[] deshake_parts = EntryPoint.deshakeSettings.Split(',');
List<String> settings = new List<string>(deshake_parts);
string[] default_values = EntryPoint.deshakeDefaultValues.Split(',');
List<String> defaults = new List<string>(default_values);
string[] current_settings = current_values.Split('|');
//current_settings[0] = current_settings[0].Substring(1);
//current_settings[66] = current_settings[66].Substring(0, 6);
List<String> current = new List<string>(current_settings);
OK_button.Click += (s, e2) =>
{
EntryPoint.deshaker_param = string.Join("|", current.ToArray());
};
dataGridView1.Rows.Clear();
for (int i = 0; i < settings.Count; i++)
{
dataGridView1.Rows.Add(i + 1, settings[i], defaults[i], current[i]);
}
}
However, if you call Load_Tweaker more than once you need to manage the addition and removal of the handler for each call. It's a little more complicated, but not too bad.
private EventHandler okButtonClick = null;
private void Load_Tweaker(object sender, EventArgs e)
{
string[] deshake_parts = EntryPoint.deshakeSettings.Split(',');
List<String> settings = new List<string>(deshake_parts);
string[] default_values = EntryPoint.deshakeDefaultValues.Split(',');
List<String> defaults = new List<string>(default_values);
string[] current_settings = current_values.Split('|');
//current_settings[0] = current_settings[0].Substring(1);
//current_settings[66] = current_settings[66].Substring(0, 6);
List<String> current = new List<string>(current_settings);
if (okButtonClick != null)
{
OK_button.Click -= okButtonClick;
}
okButtonClick = (s, e2) =>
{
EntryPoint.deshaker_param = string.Join("|", current.ToArray());
};
OK_button.Click += okButtonClick;
dataGridView1.Rows.Clear();
for (int i = 0; i < settings.Count; i++)
{
dataGridView1.Rows.Add(i + 1, settings[i], defaults[i], current[i]);
}
}
I am trying to add an autocomplete feature to a textbox, the results are coming from a database. They come in the format of
[001] Last, First Middle
Currently you must type [001]... to get the entries to show. So the problem is that I want it to complete even if I type the firstname first. So if an entry was
[001] Smith, John D
if I started typing John then this entry should show up in the results for the auto complete.
Currently the code looks something like
AutoCompleteStringCollection acsc = new AutoCompleteStringCollection();
txtBox1.AutoCompleteCustomSource = acsc;
txtBox1.AutoCompleteMode = AutoCompleteMode.Suggest;
txtBox1.AutoCompleteSource = AutoCompleteSource.CustomSource;
....
if (results.Rows.Count > 0)
for (int i = 0; i < results.Rows.Count && i < 10; i++)
{
row = results.Rows[i];
acsc.Add(row["Details"].ToString());
}
}
results is a dataset containing the query results
The query is a simple search query using the like statement. The correct results are returned if we do not use the autocomplete and just toss the results into an array.
Any advice?
EDIT:
Here is the query that returns the results
SELECT Name from view_customers where Details LIKE '{0}'
With {0} being the placeholder for the searched string.
The existing AutoComplete functionality only supports searching by prefix. There doesn't seem to be any decent way to override the behavior.
Some people have implemented their own autocomplete functions by overriding the OnTextChanged event. That's probably your best bet.
For example, you can add a ListBox just below the TextBox and set its default visibility to false. Then you can use the OnTextChanged event of the TextBox and the SelectedIndexChanged event of the ListBox to display and select items.
This seems to work pretty well as a rudimentary example:
public Form1()
{
InitializeComponent();
acsc = new AutoCompleteStringCollection();
textBox1.AutoCompleteCustomSource = acsc;
textBox1.AutoCompleteMode = AutoCompleteMode.None;
textBox1.AutoCompleteSource = AutoCompleteSource.CustomSource;
}
private void button1_Click(object sender, EventArgs e)
{
acsc.Add("[001] some kind of item");
acsc.Add("[002] some other item");
acsc.Add("[003] an orange");
acsc.Add("[004] i like pickles");
}
void textBox1_TextChanged(object sender, System.EventArgs e)
{
listBox1.Items.Clear();
if (textBox1.Text.Length == 0)
{
hideResults();
return;
}
foreach (String s in textBox1.AutoCompleteCustomSource)
{
if (s.Contains(textBox1.Text))
{
Console.WriteLine("Found text in: " + s);
listBox1.Items.Add(s);
listBox1.Visible = true;
}
}
}
void listBox1_SelectedIndexChanged(object sender, System.EventArgs e)
{
textBox1.Text = listBox1.Items[listBox1.SelectedIndex].ToString();
hideResults();
}
void listBox1_LostFocus(object sender, System.EventArgs e)
{
hideResults();
}
void hideResults()
{
listBox1.Visible = false;
}
There's a lot more you could do without too much effort: append text to the text box, capture additional keyboard commands, and so forth.
If you decide to use a query that is based on user input make sure you use SqlParameters to avoid SQL Injection attacks
SqlCommand sqlCommand = new SqlCommand();
sqlCommand.CommandText = "SELECT Name from view_customers where Details LIKE '%" + #SearchParam + "%'";
sqlCommand.Parameters.AddWithValue("#SearchParam", searchParam);
Here's an implementation that inherits the ComboBox control class, rather than replacing the whole combo-box with a new control. It displays its own drop-down when you type in the text box, but clicking to show the drop-list is handled as before (i.e. not with this code). As such you get that proper native control and look.
Please use it, modify it and edit the answer if you would like to improve it!
class ComboListMatcher : ComboBox, IMessageFilter
{
private Control ComboParentForm; // Or use type "Form"
private ListBox listBoxChild;
private int IgnoreTextChange;
private bool MsgFilterActive = false;
public ComboListMatcher()
{
// Set up all the events we need to handle
TextChanged += ComboListMatcher_TextChanged;
SelectionChangeCommitted += ComboListMatcher_SelectionChangeCommitted;
LostFocus += ComboListMatcher_LostFocus;
MouseDown += ComboListMatcher_MouseDown;
HandleDestroyed += ComboListMatcher_HandleDestroyed;
}
void ComboListMatcher_HandleDestroyed(object sender, EventArgs e)
{
if (MsgFilterActive)
Application.RemoveMessageFilter(this);
}
~ComboListMatcher()
{
}
private void ComboListMatcher_MouseDown(object sender, MouseEventArgs e)
{
HideTheList();
}
void ComboListMatcher_LostFocus(object sender, EventArgs e)
{
if (listBoxChild != null && !listBoxChild.Focused)
HideTheList();
}
void ComboListMatcher_SelectionChangeCommitted(object sender, EventArgs e)
{
IgnoreTextChange++;
}
void InitListControl()
{
if (listBoxChild == null)
{
// Find parent - or keep going up until you find the parent form
ComboParentForm = this.Parent;
if (ComboParentForm != null)
{
// Setup a messaage filter so we can listen to the keyboard
if (!MsgFilterActive)
{
Application.AddMessageFilter(this);
MsgFilterActive = true;
}
listBoxChild = listBoxChild = new ListBox();
listBoxChild.Visible = false;
listBoxChild.Click += listBox1_Click;
ComboParentForm.Controls.Add(listBoxChild);
ComboParentForm.Controls.SetChildIndex(listBoxChild, 0); // Put it at the front
}
}
}
void ComboListMatcher_TextChanged(object sender, EventArgs e)
{
if (IgnoreTextChange > 0)
{
IgnoreTextChange = 0;
return;
}
InitListControl();
if (listBoxChild == null)
return;
string SearchText = this.Text;
listBoxChild.Items.Clear();
// Don't show the list when nothing has been typed
if (!string.IsNullOrEmpty(SearchText))
{
foreach (string Item in this.Items)
{
if (Item != null && Item.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase))
listBoxChild.Items.Add(Item);
}
}
if (listBoxChild.Items.Count > 0)
{
Point PutItHere = new Point(this.Left, this.Bottom);
Control TheControlToMove = this;
PutItHere = this.Parent.PointToScreen(PutItHere);
TheControlToMove = listBoxChild;
PutItHere = ComboParentForm.PointToClient(PutItHere);
TheControlToMove.Show();
TheControlToMove.Left = PutItHere.X;
TheControlToMove.Top = PutItHere.Y;
TheControlToMove.Width = this.Width;
int TotalItemHeight = listBoxChild.ItemHeight * (listBoxChild.Items.Count + 1);
TheControlToMove.Height = Math.Min(ComboParentForm.ClientSize.Height - TheControlToMove.Top, TotalItemHeight);
}
else
HideTheList();
}
/// <summary>
/// Copy the selection from the list-box into the combo box
/// </summary>
private void CopySelection()
{
if (listBoxChild.SelectedItem != null)
{
this.SelectedItem = listBoxChild.SelectedItem;
HideTheList();
this.SelectAll();
}
}
private void listBox1_Click(object sender, EventArgs e)
{
var ThisList = sender as ListBox;
if (ThisList != null)
{
// Copy selection to the combo box
CopySelection();
}
}
private void HideTheList()
{
if (listBoxChild != null)
listBoxChild.Hide();
}
public bool PreFilterMessage(ref Message m)
{
if (m.Msg == 0x201) // Mouse click: WM_LBUTTONDOWN
{
var Pos = new Point((int)(m.LParam.ToInt32() & 0xFFFF), (int)(m.LParam.ToInt32() >> 16));
var Ctrl = Control.FromHandle(m.HWnd);
if (Ctrl != null)
{
// Convert the point into our parent control's coordinates ...
Pos = ComboParentForm.PointToClient(Ctrl.PointToScreen(Pos));
// ... because we need to hide the list if user clicks on something other than the list-box
if (ComboParentForm != null)
{
if (listBoxChild != null &&
(Pos.X < listBoxChild.Left || Pos.X > listBoxChild.Right || Pos.Y < listBoxChild.Top || Pos.Y > listBoxChild.Bottom))
{
this.HideTheList();
}
}
}
}
else if (m.Msg == 0x100) // WM_KEYDOWN
{
if (listBoxChild != null && listBoxChild.Visible)
{
switch (m.WParam.ToInt32())
{
case 0x1B: // Escape key
this.HideTheList();
return true;
case 0x26: // up key
case 0x28: // right key
// Change selection
int NewIx = listBoxChild.SelectedIndex + ((m.WParam.ToInt32() == 0x26) ? -1 : 1);
// Keep the index valid!
if (NewIx >= 0 && NewIx < listBoxChild.Items.Count)
listBoxChild.SelectedIndex = NewIx;
return true;
case 0x0D: // return (use the currently selected item)
CopySelection();
return true;
}
}
}
return false;
}
}
THIS WILL GIVE YOU THE AUTOCOMPLETE BEHAVIOR YOU ARE LOOKING FOR.
The attached example is a complete working form, Just needs your data source, and bound column names.
using System;
using System.Data;
using System.Windows.Forms;
public partial class frmTestAutocomplete : Form
{
private DataTable maoCompleteList; //the data table from your data source
private string msDisplayCol = "name"; //displayed text
private string msIDcol = "id"; //ID or primary key
public frmTestAutocomplete(DataTable aoCompleteList, string sDisplayCol, string sIDcol)
{
InitializeComponent();
maoCompleteList = aoCompleteList
maoCompleteList.CaseSensitive = false; //turn off case sensitivity for searching
msDisplayCol = sDisplayCol;
msIDcol = sIDcol;
}
private void frmTestAutocomplete_Load(object sender, EventArgs e)
{
testCombo.DisplayMember = msDisplayCol;
testCombo.ValueMember = msIDcol;
testCombo.DataSource = maoCompleteList;
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; //do this before repainting the text, as it changes the dropdown text.
//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 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;
}
If you're running that query (with {0} being replaced by the string entered), you might need:
SELECT Name from view_customers where Details LIKE '%{0}%'
LIKE still needs the % character... And yes, you should use parameters rather than trusting the user's input :)
Also, you seem to be returning the Name column, but querying on the Details column. So if someone types in "John Smith", if that's not in the Details column you won't get what you want back.
Two methods were successful in the autoComplete textBox control with SQL:
but you should do the following:
a- create new project
b- add Component class to project and delete component1.designer "according to the name you give to component class"
c- download "Download sample - 144.82 KB"
and open it and open AutoCompleteTextbox class from AutoCompleteTextbox.cs
d- select all as illustrated in the image and copy it to current component class
http://i.stack.imgur.com/oSqCa.png
e- Final - run project and stop to view new AutoCompleteTextbox in toolBox.
Now you can add the following two method that you can use SQL with them
1- in Form_Load
private void Form1_Load(object sender, EventArgs e)
{
SqlConnection cn = new SqlConnection(#"server=.;database=My_dataBase;integrated security=true");
SqlDataAdapter da = new SqlDataAdapter(#"SELECT [MyColumn] FROM [my_table]", cn);
DataTable dt = new DataTable();
da.Fill(dt);
List<string> myList = new List<string>();
foreach (DataRow row in dt.Rows)
{
myList.Add((string)row[0]);
}
autoCompleteTextbox1.AutoCompleteList = myList;
}
2- in TextChanged Event
private void autoCompleteTextbox_TextChanged(object sender, EventArgs e)
{
SqlConnection cn = new SqlConnection(#"server=.;database=My_dataBase;integrated security=true");
SqlDataAdapter da = new SqlDataAdapter(#"SELECT [MyColumn] FROM [my_table]", cn);
DataTable dt = new DataTable();
da.Fill(dt);
List<string> myList = new List<string>();
foreach (DataRow row in dt.Rows)
{
myList.Add((string)row[0]);
}
autoCompleteTextbox2.AutoCompleteList = myList;
}