Reorder a winforms listbox using drag and drop? - c#

Is this a simple process?
I'm only writing a quick hacky UI for an internal tool.
I don't want to spend an age on it.

Here's a quick down and dirty app. Basically I created a Form with a button and a ListBox. On button click, the ListBox gets populated with the date of the next 20 days (had to use something just for testing). Then, it allows drag and drop within the ListBox for reordering:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
this.listBox1.AllowDrop = true;
}
private void button1_Click(object sender, EventArgs e)
{
for (int i = 0; i <= 20; i++)
{
this.listBox1.Items.Add(DateTime.Now.AddDays(i));
}
}
private void listBox1_MouseDown(object sender, MouseEventArgs e)
{
if (this.listBox1.SelectedItem == null) return;
this.listBox1.DoDragDrop(this.listBox1.SelectedItem, DragDropEffects.Move);
}
private void listBox1_DragOver(object sender, DragEventArgs e)
{
e.Effect = DragDropEffects.Move;
}
private void listBox1_DragDrop(object sender, DragEventArgs e)
{
Point point = listBox1.PointToClient(new Point(e.X, e.Y));
int index = this.listBox1.IndexFromPoint(point);
if (index < 0) index = this.listBox1.Items.Count-1;
object data = e.Data.GetData(typeof(DateTime));
this.listBox1.Items.Remove(data);
this.listBox1.Items.Insert(index, data);
}

7 Years Late. But for anybody new, here is the code.
private void listBox1_MouseDown(object sender, MouseEventArgs e)
{
if (this.listBox1.SelectedItem == null) return;
this.listBox1.DoDragDrop(this.listBox1.SelectedItem, DragDropEffects.Move);
}
private void listBox1_DragOver(object sender, DragEventArgs e)
{
e.Effect = DragDropEffects.Move;
}
private void listBox1_DragDrop(object sender, DragEventArgs e)
{
Point point = listBox1.PointToClient(new Point(e.X, e.Y));
int index = this.listBox1.IndexFromPoint(point);
if (index < 0) index = this.listBox1.Items.Count - 1;
object data = listBox1.SelectedItem;
this.listBox1.Items.Remove(data);
this.listBox1.Items.Insert(index, data);
}
private void itemcreator_Load(object sender, EventArgs e)
{
this.listBox1.AllowDrop = true;
}

The first time it takes a few hours if you never implemented drag and drop, want to get it done right and have to read through the docs. Especially the immediate feedback and restoring the list if the user cancels the operation require some thoughts. Encapsulating the behavior into a reusable user control will take some time, too.
If you have never done drag and drop at all, have a look at this drag and drop example from the MSDN. This would be a good starting point and it should take you maybe half a day to get the thing working.

This relies on #BFree's answer above - thanks it helped a lot.
I ran into an error when trying to use the solution because I was using a DataSource for my listbox. Just for completeness, you get this error if you try to remove or add an item to the listbox directly:
// Causes error
this.listBox1.Items.Remove(data);
Error:
System.ArgumentException: 'Items collection cannot be modified when the DataSource property is set.'
Solution: Update the datasource itself, and then rebind to your listbox. Program.SelectedReports is a BindingList.
Code:
private void listboxSelectedReports_DragDrop(object sender, DragEventArgs e)
{
// Get the point where item was dropped.
Point point = listboxSelectedReports.PointToClient(new Point(e.X, e.Y));
// Get the index of the item where the point was dropped
int index = this.listboxSelectedReports.IndexFromPoint(point);
// if index is invalid, put item at the end of the list.
if (index < 0) index = this.listboxSelectedReports.Items.Count - 1;
// Get the item's data.
ReportModel data = (ReportModel)e.Data.GetData(typeof(ReportModel));
// Update the property we use to control sorting within the original datasource
int newSortOrder = 0;
foreach (ReportModel report in Program.SelectedReports) {
// match sorted item on unique property
if (data.Id == report.Id)
{
report.SortOrder = index;
if (index == 0) {
// only increment our new sort order if index is 0
newSortOrder += 1;
}
} else {
// skip our dropped item's index
if (newSortOrder == index) {
newSortOrder += 1;
}
report.SortOrder = newSortOrder;
newSortOrder += 1;
}
}
// Sort original list and reset the list box datasource.
// Note: Tried other things, Reset(), Invalidate(). Updating DataSource was only way I found that worked??
Program.SelectedReports = new BindingList<ReportModel>(Program.SelectedReports.OrderBy(x => x.SortOrder).ToList());
listboxSelectedReports.DataSource = Program.SelectedReports;
listboxSelectedReports.DisplayMember = "Name";
listboxSelectedReports.ValueMember = "ID";
}
Other notes:
BindingList is under this namespace:
using System.ComponentModel;
When dynamically adding items to the list, make sure you populate your sorting property. I used an integer field 'SortOrder'.
When you remove an item, I don't have to worry about updating the Sorting property, as it will just create a number gap which is ok in my situation, YMMV.
To be honest, there could be a better sorting algorithm other than a foreach loop, but in my situation, I am dealing with a very limited number of items.

An alternative is using the list-view control, which is the control Explorer uses to display the contents of folders. It is more complicated, but implements item dragging for you.

Related

Get index of a selected RadioButton in radioGroup

i want to find an index of a selected RadioButton in RadioGroup. I attached next single method to each RadioButton in the group:
private void radio_button_CheckedChanged(object sender, EventArgs e){
if (sender.GetType() != typeof(RadioButton)) return;
if (((RadioButton)sender).Checked){
int ndx = my_radio_group.Controls.IndexOf((Control)sender);
// change something based on the ndx
}
}
It is important to me that lower radioButton must have lower index, starting from zero. And seems it is working, but i am not sure if this is a good solution. Maybe there is more betufilul way to do the same.
This will give you the Checked RadioButton:
private void radioButtons_CheckedChanged(object sender, EventArgs e)
{
RadioButton rb = sender as RadioButton;
if (rb.Checked)
{
Console.WriteLine(rb.Text);
}
}
Any indices in the Controls collection of its Parent are highly volatile.
You could access it like this: rb.Parent.Controls.IndexOf(rb)
If you want a relatively stable ID besides the Name and the Text you can put it in the Tag.
Obviously you need to hook up this event to all the RadionButtons in the group.
No type checks are really necessary (or imo recommended,) as only a RadioButton can (or rather: must ever) trigger this event.
To obtain index ideally you want to have controls arranged as collection. If you can add controls from code behind than that's as easy as
List<RadionButton> _buttons = new List<RadioButton>();
_buttons.Add(new RadioButton() { ... });
_buttons.Add(new RadioButton() { ... });
...
If you want to use form designed, then perhaps creating this list in form constructor is an alternative:
List<RadioButtons> _list = new List<RadioButton>();
public Form1()
{
InitializeComponent();
_list.Add(radioButton1);
_list.Add(radioButton2);
...
}
Then the actual task to obtain index is as simple as:
void radioButton_CheckedChanged(object sender, EventArgs e)
{
var index = _list.IndexOf(sender);
...
}
//----checked change----
private void radioButtons_CheckedChanged(object sender, EventArgs e)
{
int ndx = 0;
var buttons = RdoGroup.Controls.OfType<RadioButton>()
.FirstOrDefault(n => n.Checked);
//-----in initialize set radioButton tags : this.radioButton1.Tag = "1";------
if (buttons.Tag != null) ndx=Convert.ToInt32( buttons.Tag.ToString());
//--------do some thing by index----------
}

Clear(remove) combobox items throws exception

I have combobox which is correctly populated with some field ID when button is clicked.
private void Button_Click(object sender, RoutedEventArgs e)
{
results.Items.Add(ID);
}
Now I want when I change some value to delete previous value (or values in case I have multiple values in combobox) but I am always getting exception (if some value is already selected in Combo Box)
I tried to add in that method on the top this:
results.Items.Clear();
and I tried this:
for (int i = 0; i < results.Items.Count; i++)
{
results.Items.RemoveAt(i);
i--;
}
But always getting exception:
System.ArgumentException: Value does not fall within the expected range.
at MS.Internal.XcpImports.MethodEx(IntPtr ptr, String name, CValue[] cvData)
at MS.Internal.XcpImports.MethodPack(IntPtr objectPtr, String methodName, Object[] rawData)
at MS.Internal.XcpImports.Collection_Add[T](PresentationFrameworkCollection'1 collection, Object value)
at System.Windows.PresentationFrameworkCollection'1.AddImpl(Object value)
at System.Windows.Controls.ItemCollection.AddImpl(Object value)
at System.Windows.Controls.ItemCollection.AddInternal(Object value)
at System.Windows.PresentationFrameworkCollection'1.Add(T value)
at SXPCreateIncident3.SilverlightControl1.results_SelectionChanged(Object sender, SelectionChangedEventArgs e)
at System.Windows.Controls.Primitives.Selector.OnSelectionChanged(SelectionChangedEventArg
If I don't have this part with Clear (Remove) then combobox has more elements on every button Click but I need to clear previous content when button is clicked.
Did you try unselecting all items before deleting:
results.SelectedIndex = -1;
results.Items.Clear();
And in case Clear would still cause some trouble, shouldn't your second method be:
for (int i = results.Items.Count - 1; i >= 0; i--)
{
results.Items.RemoveAt(i);
}
I'm not entirely sure how result.Items is bound to you Combobox but you might try to replace the unwanted item with a new one:
private void Button_Click(object sender, RoutedEventArgs e)
{
// itemToRemove should already be set
var index = result.Items.IndexOf(itemToRemove);
results.Items[index ] = ID;
}
To remove multiple items, do not use an iterator. Removing things from a collection while using an iterator messes the iterator up. You could however do this:
private void Button_Click(object sender, RoutedEventArgs e)
{
for(var i = 0; i < result.Items.Count; i++)
{
// itemsToRemove should be populated with the IDs you want to remove.
if(itemsToRemove.Contains(result.Items[i])
{
result.RemoveAt(i);
}
}
result.Items.Add(ID);
}
This loop won't get messed up because every time the expression i < result.Items.Count is evaluated and Count be one less then the previous Count when an ID has been removed.
EDIT
To clear the combobox and populate it with new items you'll have to provide a new ItemsSource for the combobox:
private void Button_Click(object sender, RoutedEventArgs e)
{
results.ItemsSource = null;
results.ItemsSource = new List<SomeType>(); // replace SomeType with the type of ID.
results.Items.Add(ID);
}

Drag multiple files

I have a ListBox with a list of filepaths, which has the property SelectionMode set to MultiExtended. Thus, I can select many items from this list.
Now, I want to Drag and Drop files starting from those paths in the destination folder where I drop them.
My code:
private void Form1_Load(object sender, EventArgs e)
{
// populate the FileList
// i.e. FileList.Items.Add("d:/Samples/file-00" + i + ".wav");
this.FileList.MouseDown += new MouseEventHandler(FileList_MouseDown);
this.FileList.DragOver += new DragEventHandler(FileList_DragOver);
}
void FileList_DragOver(object sender, DragEventArgs e)
{
e.Effect = DragDropEffects.Copy;
}
void FileList_MouseDown(object sender, MouseEventArgs e)
{
List<string> filesToDrag = new List<string>();
foreach (var item in FileList.SelectedItems)
{
filesToDrag.Add(item.ToString().Trim());
}
this.FileList.DoDragDrop(new DataObject(DataFormats.FileDrop,
filesToDrag.ToArray()), DragDropEffects.Copy);
}
it works perfect if I select and drop 1 single line/file from the ListBox to the destination folder.
Instead, if I do a multiple selection and I try to drag and drop, it can just select that one line where I start to drag. Seems that MouseDown prevent this?
How would you fix the problem?
Doing this with a ListBox seems to be ridiculously hard. Actually I haven't found a solution at all..
Use a ListView instead! It is easy as pie, using the ItemDrag event and is a much better control anyway.. I can't count how often I had to change from a 'cheap' ListBox to ListView because I needed this or that 'little' extra..
Here is your code moved to a ItemDrag:
private void listView1_ItemDrag(object sender, ItemDragEventArgs e)
{
List<string> filesToDrag = new List<string>();
foreach (var item in listView1.SelectedItems)
{
filesToDrag.Add(item.ToString().Trim());
}
this.listView1.DoDragDrop(new DataObject(DataFormats.FileDrop,
filesToDrag.ToArray()), DragDropEffects.Copy);
}
Note that this only solves the problem of the MouseDown changing the selection. It is in itself not a guarantuee that the actual copying will work.
I found this interesting article that proposes a solution. Maybe you don't need it, as you have said that you got copying one file already working..
Yeah...I don't think there's a good way around that problem. When you click again to initiate the drag it will toggle that item. We don't know that the user actually wants to do a drag until the mouse is held down and moved.
One potential solution is to initiate the drag/drop from something else, just somehow make it really clear that is what the user should drag. Here I'm using a Label instead of the ListBox:
void label1_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button == System.Windows.Forms.MouseButtons.Left)
{
List<string> filesToDrag = new List<string>();
foreach (var item in FileList.SelectedItems)
{
filesToDrag.Add(item.ToString().Trim());
}
if (filesToDrag.Count > 0)
{
this.FileList.DoDragDrop(new DataObject(DataFormats.FileDrop,
filesToDrag.ToArray()), DragDropEffects.Copy);
}
else
{
MessageBox.Show("Select Files First!");
}
}
}
You have to be picky about the mouse down and mouse move activities. When it is within the graphics rectangle of your listbox, you'll want normal behavior. When it is outside the bounds of this rectangle, you'll want drag/drop functionality. You can try the pseudo code below:
MouseDown(sender, e)
{
var x = <your sender as control>.ItemFromPoint(.....)
this.mouseLocation = x == null ? x : e.Location;
}
MouseMove(sender, e)
{
if control rectangle doesn't contain the current location then
<your sender as control>.Capture = false
DoDragDrop
}

Moving item from one list box to another list box (c# webforms)

I am encountering an odd issue whereby I can move items from one list box to another, but cannot move any items back to the original list box. Here is my code:
private void MoveListBoxItems(ListBox from, ListBox to)
{
for(int i = 0; i < first_listbox.Items.Count; i++)
{
if (first_listbox.Items[i].Selected)
{
to.Items.Add(from.SelectedItem);
from.Items.Remove(from.SelectedItem);
}
}
from.SelectedIndex = -1;
to.SelectedIndex = -1;
}
protected void Button2_Click(object sender, EventArgs e)
{
MoveListBoxItems(first_listbox, second_listbox);
}
protected void Button1_Click(object sender, EventArgs e)
{
MoveListBoxItems(second_listbox, first_listbox);
}
The button2 event works fine, however the button1 event does not. The list boxes are not data bound and I have manually added items to them.
Maybe there is something very obvious that I am missing here?
Thanks for your help in advance.
Change it to this:
private void MoveListBoxItems(ListBox from, ListBox to)
{
for(int i = 0; i < from.Items.Count; i++)
{
if (from.Items[i].Selected)
{
to.Items.Add(from.SelectedItem);
from.Items.Remove(from.SelectedItem);
// should probably be this:
to.Items.Add(from.Items[i]);
from.Items.Remove(from.Items[i]);
}
}
from.SelectedIndex = -1;
to.SelectedIndex = -1;
}
Your original method was using first_listbox in these two places, instead of from. Also, I imagine your code does not work if more than one item is selected.
Change your for loops to iterate over the local parameter from, not specifically the first_listbox:
private void MoveListControlItems(ListControl from, ListControl to)
{
for(int i = 0; i < from.Items.Count; i++)
{
if (from.Items[i].Selected)
{
to.Items.Add(from.Items[i]);
from.Items.Remove(from.Items[i]);
}
}
from.SelectedIndex = -1;
to.SelectedIndex = -1;
}
You also want to switch the add and remove if you want to move multiple items at a time.
Just another thought, though it is mostly personal preference, if you switch the parameter types to ListControl you can use the same method for ComboBox's as well.

How can I create only one event handler method for multiple controls?

I have 15 comboBox'es, and I do not want to create an event handler for each. How do I make just one procedure and tie all Combobox'es to it?
private void cbSlots0_SelectedIndexChanged(object sender, EventArgs e)
{
var item = ConfigClass.Slots["0"][cbSlots0.SelectedIndex];
ConfigClass.Slots["0"].Insert(0, item);
ConfigClass.Slots["0"].RemoveAt(cbSlots0.SelectedIndex + 1);
}
private void cbSlots1_SelectedIndexChanged(object sender, EventArgs e)
{
var item = ConfigClass.Slots["1"][cbSlots1.SelectedIndex];
ConfigClass.Slots["1"].Insert(1, item);
ConfigClass.Slots["1"].RemoveAt(cbSlots1.SelectedIndex + 1);
}
Correct answer:
var cb = ((ComboBox)sender);
var tag = int.Parse(cb.Tag.ToString());
var item = ConfigClass.Slots[tag.ToString()][cb.SelectedIndex];
ConfigClass.Slots[tag.ToString()].Insert(tag, item);
ConfigClass.Slots[tag.ToString()].RemoveAt(cb.SelectedIndex + 1);
You can give each ComboBox a distinct Tag, which contains the number of the entry in the ConfigClass, and then use that like so:
private void cbSlots0_SelectedIndexChanged(object sender, EventArgs e)
{
int tag = (int)((ComboBox)sender).Tag;
var item = ConfigClass.Slots[tag.ToString()][cbSlots0.SelectedIndex];
ConfigClass.Slots[tag.ToString()].Insert(tag, item);
ConfigClass.Slots[tag.ToString()].RemoveAt(cbSlots0.SelectedIndex + 1);
}
The tag can contain any data you want, so if you need something more complex stored in there, that's also a possibility.
I would recommend one event handler for all ComboBoxes. Afterwards, within your event handler, use the sender reference to decide which slot to use:
private void allComboBoxesSelectedIndesChanged(object sender, EventArgs e)
{
int index = 0; // Or string as you have shown in your example.
if (sender == cbSlots0)
index = 0;
else if (sender == cbSlots1)
index = 1;
/// And so on for any other comboBox
var item = ConfigClass.Slots[index][((ComboBox) sender).SelectedIndex];
ConfigClass.Slots[index].Insert(index, item);
ConfigClass.Slots[index].RemoveAt(((ComboBox) sender).SelectedIndex +1);
}
This is relatively simple. You create a single SelectedIndexChanged event handler method, and then wire that up to all of the combo box controls.
The way you distinguish between the controls inside of the method at run-time is by checking the value of the sender parameter. You'll have to cast it to a ComboBox control, but that's safe because you know that you didn't wire up any non-combobox controls to that event handler. Then you'll be able to access all the properties of the combobox that raised the event you're handling.
Tie each item in your markup to the same SelectedIndexChangedEvent and cast the sender as your item. So, in your code, look for all of the unique event names (ie. cbSlots0_SelectedIndexChanged, cbSlots1_SelectedIndexChanged, etc) and rename them to the single event name (eg. cbSlotsSelectedIndexChanged).
I think this is right. Verify.
CODE:
private void cbSlotsSelectedIndexChanged(object sender, EventArgs e)
{
ComboBox cBox = (ComboBox) sender;
int tag = (int)cBox.Tag;
var item = ConfigClass.Slots[tag.ToString()][cBox.SelectedIndex];
ConfigClass.Slots[tag.ToString()].Insert(tag, item);
ConfigClass.Slots[tag.ToString()].RemoveAt(item.SelectedIndex + 1);
}
UPDATE:
I revised my post as requested
private void cbSlotsSelectedIndexChanged(object sender, EventArgs e)
{
var cb = ((ComboBox)sender);
var tag = int.Parse(cb.Tag.ToString());
var item = ConfigClass.Slots[tag.ToString()][cb.SelectedIndex];
ConfigClass.Slots[tag.ToString()].Insert(tag, item);
ConfigClass.Slots[tag.ToString()].RemoveAt(cb.SelectedIndex + 1);
}

Categories