Code to delete a nested TabPage isn't always working - c#

I wanted to implement a MiddleClick-To-Delete functionality, like most tabbed environments have, on my application which looks like this:
It's just a single form with a TabControl that has 2x TabPage: ActiveItems and Archived Items.
Both of those pages contain their own TabControl, which the user can add as many TabPage objects as they desire.
If the user has Active Items selected and they delete a tab, it deletes the correct one. If they delete a tab from Archived Items, it also deletes the correct one. If they move a tab from Active to Archived then delete it, it is removing the wrong tab. So in the screenshot above, you can't see Archived Items but there are 2 tabs there already. If I move 13571 from Active -> Archive, then delete it (tab #3, index #2), it removes tab #2, index #1.
// Both the tcActive and tcArchived, the TabControls on tcRoot's two Active Items
// and Archived Items tab pages, subscribe to this event
private void tc_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button != System.Windows.Forms.MouseButtons.Left)
tabPageClicked = e.Location;
if (e.Button == MouseButtons.Middle)
DeleteTab();
}
// Passes the TabControl and TabPageIndex to be deleted
private void DeleteTab()
{
int tpi = GetTabIndex(tabPageClicked);
if (tcActiveArchive.SelectedIndex.Equals(0))
DeleteTab(tcActive, tpi);
else
DeleteTab(tcArchived, tpi);
}
// Gets the TabPage index from either Active Items page or Archived Items page
private int GetTabIndex(Point p)
{
if (tcActiveArchive.SelectedIndex.Equals(0))
return tcActive.TabPages.IndexOf(tcActive.TabPages.Cast<TabPage>().Where((t, i) => tcActive.GetTabRect(i).Contains(p)).First());
else
return tcArchived.TabPages.IndexOf(tcArchived.TabPages.Cast<TabPage>().Where((t, i) => tcArchived.GetTabRect(i).Contains(p)).First());
}
// Actual removal of TabPage at index tabIndex from TabControl tc
private void DeleteTab(TabControl tc, int tabIndex)
{
lastDeleteWasActiveTab = (tcActiveArchive.SelectedIndex.Equals(0));
//Tab object just stores a string, string[] and bool
Tab deletedTab = new Tab(tc.TabPages[tabIndex].Text, ((TextBox)tc.TabPages[tabIndex].Controls[0]).Lines, lastDeleteWasActiveTab);
if (lastDeleteWasActiveTab)
{
deletedActiveTabs.Push(deletedTab);
filesToDelete.Add(mantisDir + tc.TabPages[tabIndex].Text + ".txt");
}
else
{
deletedArchiveTabs.Push(deletedTab);
filesToDelete.Add(archiveDir + tc.TabPages[tabIndex].Text + ".txt");
}
tc.TabPages.RemoveAt(tabIndex);
//Try to select the tab to the right of the one that was deleted
if (tc.TabPages.Count >= tabIndex + 1)
tc.SelectedIndex = tabIndex;
else
tc.SelectedIndex = tabIndex - 1;
undoQueue.Push((lastDeleteWasActiveTab) ? UndoEventType.DeleteActive : UndoEventType.DeleteArchive);
}
Stepping through the code for the condition described above:
MouseDown event fires
DeleteTab()
GetTabIndex returns 2
DeleteTab(tcArchived, 2); is called
Tab deletedTab has all the values it's supposed to contain, indicating we're looking at the right TabPage / index.
tc.RemoveAt(2) is called
Yet it deletes the second item, not the index 2. Absolutely cannot figure out what's wrong.

Changing this line:
DeleteTab(TabControl tc, int tabIndex)
{
//other code
tc.TabPages.RemoveAt(tabIndex);
}
To:
TabPage tp = tc.TabPages[tabIndex];
tc.TabPages.Remove(tp);
Seems like it works for the use case described in my original post, with minimal testing. But I'd really like to know why RemoveAt(tabIndex) isn't working.

Related

UWP - Listbox scrolls to top after navigating within a frame on the same page

OK, tricky one to explain, but its driving me crazy... Its a UWP App on Windows 10:
I have a main page of which a large part is a Frame (where the main content pages are displayed) on the right is a user control which has a list box in it - when selecting an item in the list it loads the main content page into the frame using a user control event which then calls the Navigate method on the frame - all works fine, except... if you have scrolled down the list then click on an item, the page loads but the listbox scrolls to the top of list - really frustrating!! I can't see why it does this or understand what is going on - can anyone shed some light please?
I know its not reloading the contents and the selecteditem remains selected and does not change.
I'm not familiar with Unity, but after some research in your project, I think that each time you select one Item, you reload all your items in ListBox. For example you can take a look at your UserControl named "PersonPicker":
private void cbCategory_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (isLoaded)
people.AddFilterAndOrder("Person Category," + ((ViewModel.SystemConfiguration.SystemData.WorkCategory)cbCategory.SelectedItem).PluralTitle, loadModel: true);
}
Then I found your AddFilterAndOrder method in BaseListVM:
public void AddFilterAndOrder(string filter = "", string order = "", bool loadModel = false)
{
if (filter != "")
{
string[] items = filter.Split(';');
foreach (string i in items)
{
string[] pair = i.Split(',');
if (pair[1] == "")
filters.Remove(pair[0]);
else
if (filters.Keys.Contains(pair[0]))
filters[pair[0]] = pair[1];
else
filters.Add(pair[0], pair[1]);
}
}
if (order != "")
{
string[] items = order.Split(';');
foreach (string i in items)
{
string[] pair = i.Split(',');
if (pair[1] == "")
orders.Remove(pair[0]);
else
if (orders.Keys.Contains(pair[0]))
orders[pair[0]] = pair[1];
else
orders.Add(pair[0], pair[1]);
}
}
if (loadModel) LoadModel();
}
Since you passed "loadModel" as true to this method, LoadModel() method will be executed, I won't paste your LoadModel() method here again, but in your LoadModel method, you clear the Items and reload Items again. This is why I said you probably have refreshed your list.
So, maybe you can try:
people.AddFilterAndOrder("Person Category," + ((ViewModel.SystemConfiguration.SystemData.WorkCategory)cbCategory.SelectedItem).PluralTitle, loadModel: false);
when one Item is selected.

Way to format Form with dynamic text fields c#

Right, so I have 13 textboxes with corresponding labels that are assigned after a user decides the name from a different form (instantiated by the 'Add field...' button). The issue arises when the user wishes to delete a textbox with previously entered data, as this results in an empty space where the textbox and label originally were as visualized by the following image:
My question is: how do I make it so that when a user chooses to delete a textbox, the textbox-label pair(s) that follow it replace the deleted textbox AND shift the remaining textboxes accordingly.
Textbox-label pairs in designer:
I've thought about this problem intensively over the past few days, and have concluded that with my current knowledge of C# I am limited to solving this issue with a horrendously tedious amount of if-statements (talking hundreds - thousands here). Any and all help would be appreciated!
Current code on the X-button for first textbox-label pair:
private void xButton1_Click(object sender, EventArgs e)
{
label14.Text = "";
textBox1.Text = "";
if (label14.Text.Equals(""))
{
label14.Visible = false;
textBox1.Visible = false;
xButton.Visible = false;
label14.Text = "";
textBox1.Text = "";
}
if (!textBox2.Text.Equals(""))
{
label14.Text = label15.Text;
textBox1.Text = textBox2.Text;
}
if (!textBox2.Text.Equals("") && (textBox3.Text.Equals("")))
{
label15.Visible = false;
textBox2.Text = "";
textBox2.Visible = false;
xButton2.Visible = false;
}
}
One simple thing you could do is give all your "dynamic" controls (label, textbox, button) a similar value in their Tag property (in my example, I used the string "dynamic" for all the control Tags. This enables you to query for them easily.
Next, you could follow the logic that, anytime you delete some controls, you move all controls below the deleted ones up a distance equal to the height of the control being deleted plus whatever padding you have between the controls.
For example, when a user clicks the X button, since you know the value of the Bottom of the control that's being deleted, you could find all controls that had a matching Tag property whose Top is greater than the x button Bottom, and you can move them up.
Here's an example (this assumes that all your X buttons are mapped to this same click event):
private void buttonX_Click(object sender, EventArgs e)
{
// This is represents the distance between the bottom
// of one control to the top of the next control
// Normally it would be defined globally, and used when you
// lay out your controls.
const int controlPadding = 6;
var xButton = sender as Button;
if (xButton == null) return;
var minTopValue = xButton.Bottom;
var distanceToMoveUp = xButton.Height + controlPadding;
// Find all controls that have the Tag and are at the same height as the button
var controlsToDelete = Controls.Cast<Control>().Where(control =>
control.Tag != null &&
control.Tag.ToString() == "dynamic" &&
control.Top == xButton.Top)
.ToList();
// Delete the controls
controlsToDelete.ForEach(Controls.Remove);
// Get all controls with the same tag that are below the deleted controls
var controlsToMove = Controls.Cast<Control>().Where(control =>
control.Tag != null &&
control.Tag.ToString() == "dynamic" &&
control.Top > minTopValue);
// Move each control up the specified amount
foreach (var controlToMove in controlsToMove)
{
controlToMove.Top -= distanceToMoveUp;
}
}

TabControl AddingTab event

I have a TabControl in which I want to prevent adding existing TabPage (they are identified by a name) and instead set the SelectedTabPage to this precise tab.
I wish to know if there are an event that triggers right before a page is being added to the TabControl. If not, would using the event CollectionChanged of the TabPages (list) be a correct alternative ?
I believe the event you're looking for is the Control.ControlAdded event:
http://msdn.microsoft.com/en-us/library/system.windows.forms.control.controladded.aspx
If that also detects when things inside the tab pages themselves are added, you should be able to filter out everything but TabPage controls using the ControlEventArgs.Control property in your event handler.
To reject adding a control will be a little more complicated. Since this event seems to only be raised after the control gets added, you'll need to do something like this:
void onControlAdded(object sender, ControlEventArgs e) {
var tab = e as TabPage;
if (tab == null)
return;
this.myTabControlObject.TabPages.Remove(tab);
}
This should remove the tab, but it will likely slow the tab adding process considerably.
Try something like this, I am checking the TabControl page Collection for a page with the same name as the Page that is trying to be added, if it exists I am setting focus to the existing instance, otherwise adding the new page to the TabControl. See if something like this works for you.
private void button1_Click(object sender, EventArgs e)
{
TabPage tp = new TabPage();
tp.Name = tabPage1.Name;
var temp =tabControl1.Controls.Find(tp.Name,true);
if( temp.Length > 0)
{
tabControl1.SelectedTab = (TabPage) temp[0];
}
else
tabControl1.Controls.Add(tp);
}
Anything having to do with the ControlCollection will most likely be triggered after the control has been added.
From above link:
You can determine if a Control is a member of the collection by passing the control into the Contains method. To get the index value of the location of a Control in the collection, pass the control into the IndexOf method. The collection can be copied into an array by calling the CopyTo method.
If you want you could cleanup your code some by adding an ExtensionMethod to your TabControl Check for an existing page, set focus or add from there.
Example:
namespace ExtensionMethods
{
public static class MyExtensions
{
public static bool AddPage(this TabControl tc, TabPage tp)
{
var matchedPages = tc.Controls.Find(tp.Name, false);
if ( matchedPages.Length > 0)
{
tc.SelectedTab = (TabPage)matchedPages[0];
return true;
}
else
{
tc.TabPages.Add(tp);
tc.SelectedTab = tp;
return false;
}
}
}
}
Usage:
tabControl1.AddPage(tp);

Jumping to next tab

I have a tab control in my WPF application with multiple tabs. Each tab gives access to several buttons, text boxes, drop downs. Now before moving to the next tab valid entries in each of the controls in the tab is to be checked or jumping to the next tab should not be allowed. How can this be done?
I was able to use IsEnable property to do this. But I want it like, when I click on the next tab it should, without entering the next tab, display a warning that such and such entry in the present tab is not valid.
If you adhere to the Selected event you can do something like this:
// Keep a global variable for the previous index
int prevIndex = 0;
private void tabControl_Selected(object sender, TabControlEventArgs e)
{
TabControl tc = sender as TabControl;
if (tc != null)
{
bool letSwitchHappen = validateTabControls(tc.SelectedIndex);
if (!letSwitchHappen)
{
tc.SelectedIndex = prevIndex;
}
prevIndex = tc.SelectedIndex;
}
}
Where validateTabControls is something like:
private bool validateTabControls(int tabIndex)
{
bool validEntries = false;
// Some code here to set validEntries according to the control at tabIndex
return validEntries;
}
Take a look at this example from Josh Smith.
It shows explicitly how to do this, and Josh is well-known (and respected) in the WPF world.

How to disable a checkbox in a checkedlistbox?

I have some items in a CheckedListBox, I want to disable the CheckBox of first item in it.
i.e. I want to disable the first item in the CheckedListBox, because I want to tell the user visually that option is not available.
Combining 2 of the above partial answers worked great for me.
Add your items to the list with:
myCheckedListBox.Items.Add(myItem, myState);
Where myState is CheckState.Indeterminate for items that should be disabled.
Then add an event handler to keep those items from being changed:
myCheckedListBox.ItemCheck += (s, e) => { if (e.CurrentValue == CheckState.Indeterminate) e.NewValue = CheckState.Indeterminate; };
This does not allow you to use 'Indeterminate' in this list for its normal purpose but it does give a look very similar to what one would expect for a disabled item and it provides the correct behavior!
Though this post is pretty old, the last added answer has been submitted in April this year,
and I hope this will help someone.
I was after something similar : a checked list box that behaves like
a lot of installers, which offer a list of options where some features are required and
thus are both checked and disabled.
Thanks to this post (Can I use a DrawItem event handler with a CheckedListBox?)
I managed to do that, subclassing a CheckedListBox control.
As the OP in the linked post states, in the CheckedListBox control the OnDrawItem event is never fired,
so subclassing is necessary.
It's very basic, but it works.
This is what it looks like (the CheckBox above is for comparison) :
NOTE: the disabled item is really disabled : clicking on it has no effects whatsoever (as far as I can tell).
And this is the code :
public class CheckedListBoxDisabledItems : CheckedListBox {
private List<string> _checkedAndDisabledItems = new List<string>();
private List<int> _checkedAndDisabledIndexes = new List<int>();
public void CheckAndDisable(string item) {
_checkedAndDisabledItems.Add(item);
this.Refresh();
}
public void CheckAndDisable(int index) {
_checkedAndDisabledIndexes.Add(index);
this.Refresh();
}
protected override void OnDrawItem(DrawItemEventArgs e) {
string s = Items[e.Index].ToString();
if (_checkedAndDisabledItems.Contains(s) || _checkedAndDisabledIndexes.Contains(e.Index)) {
System.Windows.Forms.VisualStyles.CheckBoxState state = System.Windows.Forms.VisualStyles.CheckBoxState.CheckedDisabled;
Size glyphSize = CheckBoxRenderer.GetGlyphSize(e.Graphics, state);
CheckBoxRenderer.DrawCheckBox(
e.Graphics,
new Point(e.Bounds.X + 1, e.Bounds.Y + 1), // add one pixel to align the check gliph properly
new Rectangle(
new Point(e.Bounds.X + glyphSize.Width + 3, e.Bounds.Y), // add three pixels to align text properly
new Size(e.Bounds.Width - glyphSize.Width, e.Bounds.Height)),
s,
this.Font,
TextFormatFlags.Left, // text is centered by default
false,
state);
}
else {
base.OnDrawItem(e);
}
}
public void ClearDisabledItems() {
_checkedAndDisabledIndexes.Clear();
_checkedAndDisabledItems.Clear();
this.Refresh();
}
}
Use it like this:
checkedListBox.Items.Add("Larry");
checkedListBox.Items.Add("Curly");
checkedListBox.Items.Add("Moe");
// these lines are equivalent
checkedListBox.CheckAndDisable("Larry");
checkedListBox.CheckAndDisable(0);
Hope this can help someone.
Disabling items isn't a great idea, the user will have no good feedback that click the check box won't have any effect. You cannot use custom drawing to make it obvious. Best thing to do is to simply omit the item.
You can however easily defeat the user with the ItemCheck event:
private void checkedListBox1_ItemCheck(object sender, ItemCheckEventArgs e) {
if (e.Index == 0) e.NewValue = e.CurrentValue;
}
To disable any particular item use following:
checkedListBox1.SetItemCheckState(0, CheckState.Indeterminate);
SetItemCheckState takes index of item and CheckState Enum
Indeterminate is used to show shaded appearance
I know it has been a while, but I found this in my search for a list box and thought I would add it to the discussion.
If you have a listbox and want to disable all of the checkboxes so they cannot be clicked, but not disable the control so the user can still scroll etc. you can do this:
listbox.SelectionMode = SelectionMode.None
The CheckedListBox will not work in this way. CheckedListBox.Items is a collection of strings so they cannot be "disabled" as such.
Here are some discussions about possible solutions that might help you: here and here.
This works for me:
checkedListBox1.SelectionMode = SelectionMode.None;
Which means no items can be selected
None: No items can be selected.
For more info, you can check it here: SelectionMode Enumeration.
The solution is to use the event ItemChecking:
_myCheckedListBox.ItemChecking += (s, e) => e.Cancel = true;
This will cancel all the checking on every item, but you can always do more refined solution but testing the current .SelectedItem
Here's how I did it in a helpdesk application I wrote:
First, I made it so the check box was greyed out as I added it to the list during form load:
private void frmMain_Load(object sender, EventArgs e)
{
List<string> grpList = new List<string>();
ADSI objADSI = new ADSI();
grpList = objADSI.fetchGroups();
foreach (string group in grpList)
{
if (group == "SpecificGroupName")
{
chkLst.Items.Add(group, CheckState.Indeterminate);
}
else
{
chkLst.Items.Add(group);
}
}
Then I used an event so that when clicked it ensures it stays clicked:
private void chkLst_SelectedIndexChanged(object sender, EventArgs e)
{
if (chkLst.SelectedItem.ToString() == "SpecificGroupName")
{
chkLst.SetItemCheckState(chkLst.SelectedIndex, CheckState.Indeterminate);
}
}
The idea here is that on my form it's set so that the box checks on item click/select. This way I could kill two birds with one stone. I could keep this event from causing problems when the item is first checked and added during form load. Plus making it check on select allows me to use this event instead of the item checked event. Ultimately the idea is to keep it from messing up during the load.
You'll also notice that it doesn't matter what the index number is, that variable is unknown because in my app it's grabbing a list of groups from AD that exist in a specific OU.
As to whether this is a good idea or not, that's dependent on the situation. I have another app where the item to disable is dependent on another setting. In this app I just want the helpdesk to see that this group is required so they don't go removing them from it.
Try Below Code:
Private Sub CheckedListBox1_MouseUp(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles CheckedListBox1.MouseUp
If (Condition) Then
Me.CheckedListBox1.SelectedIndex = -1
End If
End Sub
I think an alternative solution, is using Telerik components.
A RadListControl can give you that option:

Categories