C# Change text on click event - c#

I have a field that currently changes text on a mouse click. This field is on multiple pages of my report, but clicking it only changes the text for that page. Here is my code for that:
const string sShowDetail2 = "Show Cumulative";
const string sHideDetail2 = "Hide Cumulative";
ArrayList expandedValues2 = new ArrayList();
// This function returns a value indicating whether a certain
// category's details should be expanded.
bool ShouldShowDetail2(int CategoryID)
{
return expandedValues2.Contains(CategoryID);
}
private void lbShowHide2_BeforePrint(object sender, PrintEventArgs e)
{
XRLabel label = (XRLabel)sender;
if (ShouldShowDetail2((int)label.Tag))
{
label.Text = sHideDetail2;
}
else
{
label.Text = sShowDetail2;
}
}
private void lbShowHide2_PreviewClick(object sender, PreviewMouseEventArgs e)
{
// Obtain the category's ID stored in the label's Tag property.
int index = (int)e.Brick.Value;
// Determine whether the current category's details are shown.
bool showDetail2 = ShouldShowDetail2(index);
// Toggle the visibility of the category's details.
if (showDetail2)
{
expandedValues2.Remove(index);
}
else
{
expandedValues2.Add(index);
}
Now I have another field that I need to simply toggle all instances of the field(every page) on click. I do not need it to read the tag of the particular one I clicked because it will be changing all of them at the same time. My problem is that I don't know how to get them all to change on mouse click. How could I modify the code above to change all instances of the label?

Here is something you could try
private void ChangeXRLabels(Control control)
{
foreach(Control childControl in control.Controls)
{
XRLabel label = childControl as XRLabel;
if(label != string.Empty)
label.Text = "Your Text Value goes Here";
else if(childControl.Controls.Count > 0)
ChangeXRLabels(childControl);
}
}

Related

Winforms ListView item selection bug

I'm making an application that uses a ListView control with MultiSelect = false. In some situations I need to prevent the user from changing the selected item. I thought it would be a simple task, but hours later I'm still trying to figure out what's going on.
So in order to have the option to "freeze" the ListView selection, I made a custom class CListView that inherits from ListView. If FreezeSelection is set to true, every time the users changes the selection, I'm trying to change it back:
public class CListView : ListView
{
public bool FreezeSelection { get; set; } = false;
bool _applyingSelectionUpdates = false;
protected override void OnSelectedIndexChanged(EventArgs e)
{
if (FreezeSelection)
{
if (_applyingSelectionUpdates)
return;
// for simplicity consider that the selected index while the selection is frozen is always 2
int selectedIndex = 2;
_applyingSelectionUpdates = true;
try
{
SelectedIndices.Clear();
if (selectedIndex >= 0)
SelectedIndices.Add(selectedIndex);
}
finally { _applyingSelectionUpdates = false; }
return;
}
base.OnSelectedIndexChanged(e);
}
}
The problem is when I set FreezeSelection back to false, and the user tries to select a different item. First of all, even if MultiSelect is false, visually it appears as there are two items selected. But programatically, when the user changes the selection, it seems there is sometimes the correct item selected, sometimes no item selected.
This behaviour is clearly a bug and I suspect what is causing this bug. When the user clicks on an item, the event SelectedIndexChanged is fired twice. Once after the SelectedIndices collection is cleared and the second time after the clicked item is added to the collection of selected items. I think the bug is caused by changing the selected items between these two events, but I need to know more about this. If MultiSelect is true and the user tries to select items with Ctrl, I have no problems.
To reproduce this bug you can use the following TestForm:
public class TestForm : Form
{
CListView listView;
CheckBox checkBox;
public TestForm()
{
listView = new() { Dock = DockStyle.Fill, View = View.Details, FullRowSelect = true, MultiSelect = false };
listView.Columns.Add("col 1");
listView.SelectedIndexChanged += ListView_SelectedIndexChanged;
Controls.Add(listView);
checkBox = new() { Dock = DockStyle.Right, Text = "freeze selection" };
checkBox.CheckedChanged += CheckBox_CheckedChanged;
Controls.Add(checkBox);
listView.Items.Add("item 1");
listView.Items.Add("item 2");
listView.Items.Add("item 3");
listView.Items.Add("item 4");
}
private void CheckBox_CheckedChanged(object? sender, EventArgs e)
{
listView.FreezeSelection = checkBox.Checked;
}
DateTime lastSelChangeTime = DateTime.MinValue;
private void ListView_SelectedIndexChanged(object? sender, EventArgs e)
{
if ((DateTime.Now - lastSelChangeTime).TotalMilliseconds > 200)
Debug.WriteLine(""); // this is just to group together what happens on a single user interaction
var indices = listView.SelectedIndices.Cast<int>().ToArray();
Debug.WriteLine("CListView fired selection changed event! "
+ DateTime.Now.ToString("h:m:s:fff") + " "
+ "{ " + string.Join(", ", indices) + " }");
lastSelChangeTime = DateTime.Now;
}
}
If you run this form:
Select the third item (with index 2)
Check "freeze selection"
Click on the forth item
Uncheck "freeze selection"
Try changing the selected item now and observe the bug
The question is how to solve this bug or how to achieve my initial goal (prevent users from selecting a different item).
Update:
To clarify, what I refered to as "a bug" is not the fact that I get two events for one selection change (I'm fine with that), it's the inconsistent behaviour between the UI and ListView.SelectedIndices after I "unfreeze" the selected index. I will demonstrate the problem with the following picture (note that each screenshot is taken after I clicked where the cursor is positioned; also the output window shows the SelectedIndices every time I get an SelectedIndexChanged event):
I use .NET 6.0.
As others have mentioned, there is no bug here as shown in this sequence of selecting Item 1, then Selecting Item2 (which first changes the selection by deselecting Item 1.
If you don't want the User to be selecting things during some arbitrary task (like waiting for a modified document to be saved), why not just set ListView.Enabled to false while you perform the work? In the testcode referenced below, I made an all-in-one for when the checkbox changes that sets the SelectionIndices collection to '2' as in your post;
There are now no issues going back to a state where freeze selection is unchecked and selecting some new item.
public TestForm()
{
InitializeComponent();
listView.MultiSelect = false;
listView.Columns.Add("col 1");
for (int i = 1; i <= 4; i++) listView.Items.Add($"Item {i}");
listView.SelectedIndexChanged += (sender, e) =>
{
richTextBox.AppendLine(
$"{DateTime.Now} : [{string.Join(", ", listView.SelectedIndices.Cast<int>())}]" );
var sel =
listView
.SelectedItems
.Cast<ListViewItem>();
if (sel.Any())
{
foreach (var item in sel)
{
richTextBox.AppendLine(item);
richTextBox.AppendLine();
}
}
else richTextBox.AppendLine("No selections", Color.Salmon);
};
checkBox.CheckedChanged += (sender, e) =>
{
listView.Enabled = !checkBox.Checked;
if (checkBox.Checked) doWork();
};
void doWork()
{
listView.SelectedIndices.Clear();
listView.SelectedIndices.Add(2);
}
}
Uses this extension for RichTextBox
static class Extensions
{
public static void AppendLine(this RichTextBox richTextBox) =>
richTextBox.AppendText($"{Environment.NewLine}");
public static void AppendLine(this RichTextBox richTextBox, object text) =>
richTextBox.AppendText($"{text}{Environment.NewLine}");
public static void AppendLine(this RichTextBox richTextBox, object text, Color color)
{
var colorB4 = richTextBox.SelectionColor;
richTextBox.SelectionColor = color;
richTextBox.AppendText($"{text}{Environment.NewLine}");
richTextBox.SelectionColor = colorB4;
}
}

ListBox deletion

I have a listbox, with 2 buttons, new and delete. new adds an item into the list box, and the delete button should delete the item out of the list box. The list box items are tied to a class that stores user entered data from text boxes below.
private void AddListBox()
{
lstCondition.BeginUpdate();
Condition cond = new Condition("");
cond.Name = string.Format("Condition {0}", _selection.NetConditions.Count + 1);
_selection.NetConditions.Add(cond);
lstCondition.EndUpdate();
lstCondition.SelectedItem = cond;
cboNetCondition.Properties.Items.Clear();
cboNetCondition.Properties.Items.AddRange(NetCondition);
cboControlType.Properties.Items.Clear();
cboControlType.Properties.Items.AddRange(ControlType);
cboFlowRate.Properties.Items.Clear();
cboFlowRate.Properties.Items.AddRange(FlowRate);
}
private void btnNew_Click(object sender, EventArgs e)
{
AddListBox();
}
the cbo items are comboboxes, whose data gets tied in the condition class to each instance of the list box.
public frmNetConditions(Condition condo, Selection selection)
{
InitializeComponent();
_selection = selection;
lstCondition.DataSource = _selection.NetConditions;
condition = _selection.NetConditions.Count;
}
private void btnDelete_Click(object sender, EventArgs e)
{
selectedCondition = (Condition)lstCondition.SelectedItem;
cboControlType.SelectedIndex = -1;
cboNetCondition.SelectedIndex = -1;
cboFlowRate.SelectedIndex = -1;
txtFlowRate.Text = string.Empty;
txtStatPressure.Text = string.Empty;
txtDampOpening.Text = string.Empty;
txtDensity.Text = string.Empty;
cboDensity.SelectedIndex = -1;
lstCondition.Items.Remove(lstCondition.SelectedItem);
lstCondition.Refresh();
}
After pressing this delete button, the listbox, still contains the item i wish to delete, im unsure why thats the case?
Update with datasource
public List<Condition> NetConditions { get { return _netconditions; } }
As already suggested, you should bind to a BindingList<Condition> instead of a List<Condition>. This allows you to change the datasource and the control (ListBox) to get notified by your changes. The code should look like this:
lstCondition.ValueMember = "ConditionId";
lstCondition.DisplayMember = "Name";
lstCondition.DataSource = NetConditions;
After defining the binding, the correct way of operating on the ListBox items is to remove from the datasource, not the ListBox itself:
// SelectedItem should be checked for null (no selection is an option)
NetCondition.Remove((Condition)lstCondition.SelectedItem);
However, if you plan to change properties from an element (so, not the list itself), the control is notified only if your element (Condition) implements INotifyPropertyChanged interface.

Selecting an item in a combo cause a cyclic call

I am writing a winform application.
On the form I have a Label and a combobox.
The combobox is populated with fonts names. When selecting a font from it, the label's font text is changing accordingly. When clicking on the label, the combobox's selecteditem is set accordingly.
The problem is when I am selecting the label, I get a cyclic call: I am setting the combobox item according to the font of the label, then the SelectedIndexChanged is fired which is in charge to set the font name of the label (ChangeLabelFont).
So the label font is again updated while it is the trigger of all those calls.
private void FontToolStripComboBox_SelectedIndexChanged(object sender, EventArgs e)
{
if (_selectedLabel == null)
{
return;
}
ChangeLabelFont(FontNameToolStripComboBox.SelectedItem.ToString(), FontSizeToolStripComboBox.SelectedItem.ToString());
}
private void SelectLabel(Point location)
{
SetComboBoxesFont(_selectedLabel.Font.Name, _selectedLabel.Font.Size);
}
private void SetComboBoxesFont(string name, float size)
{
FontNameToolStripComboBox.SelectedItem = name;
FontSizeToolStripComboBox.SelectedItem = size;
}
private void ChangeLabelFont(string name, string size)
{
if (_selectedLabel == null)
{
return;
}
FontFamily fontFamily = new FontFamily(name);
float fontSize = float.Parse(size);
_selectedLabel.Font = new Font(fontFamily, fontSize);
}
Is there a way to prevent this cyclic call? Maybe I am doing something wrong?
There is something missing in the code you're showing. I'd like to see the ChangeLabelFont method as well as the handler for the "select label" event. But I suppose it's the SelectLabel method in the end. So how about using a member flag:
private bool m_bInhibitCycle = false;
private void SelectLabel(Point location)
{
if (m_bInhibitCycle) return;
m_bInhibitCycle = true;
SetComboBoxesFont(_selectedLabel.Font.Name, _selectedLabel.Font.Size);
m_bInhibitCycle = false;
}

How do I add the tag value and the text value of multiple TextBox, only when the text has changed in WPF?

I am creating a dictionary with = Tag value, and Text of multiple TextBox in a WPF application when the "submit button" is clicked. I only want to add to the dictionary the Tag and text of the Textboxes where the text has changed.
How do I add the tag value and the text value of multiple TextBox, only when the text has changed?
The code below is what I have so far, but I am stuck:
private void Submit_Button_Click(object sender, RoutedEventArgs e)
{
Dictionary<string, string> tagDict = new Dictionary<string, string>();
foreach(Control ctrl in MainGrid.Children)
{
if (ctrl.GetType() == typeof(textBox))
{
TextBox tb = ctrl as TextBox;
if (//I am trying to get a value that represents that the Text has changed here
)
{
string tagString = tb.Tag.ToString();
string textString = tb.Text;
tagDict.Add(tagString, textString);
}
}
}
}
I think a good solution is, first, to put your dictionary somewhere else (a private in the form, maybe)., and populate it when showing the form. This will allow you to update the dictionary when the text box is changed.
Assuming this, I would create a single method to be called every time the user "leaves" the edit box, and check if the text changed, on which case I would update the dictionary.
This way, the "submit" just have to use the dictionary that will be updated already.
ex:
private Dictionary<string, string> fDic; //This must be instantiated in the initialization
//Add this method to every Leave event in all text box controls.
private void textBox_Leave(object sender, EventArgs e)
{
TextBox text = sender as TextBox;
//Check if the text changed
if (text != null)
{
if (fDic.ContainsKey((string)text.Tag))
{
if (fDic[(string)text.Tag] != text.Text)
fDic[(string)text.Tag] = text.Text;
}
else if(text.Text != null)
{
fDic[(string)text.Tag] = text.Text;
}
}
}
The former code will update the dictionary if the TAG is already there AND the text associated with the tag is DIFFERENT from the text in the text box, and if THERE IS a text in the text box and the tag isn't in the dictionary already.
I'm assuming that the "Tag" is stored in the Tag property of each text box.
Also, it will store strings formed of only spaces. If you do not want this, the code can be changed to:
private Dictionary<string, string> fDic; //This must be instantiated in the initialization
//Add this method to every Leave event in all text box controls.
private void textBox_Leave(object sender, EventArgs e)
{
TextBox text = sender as TextBox;
//Check if the text changed
if (text != null)
{
if (fDic.ContainsKey((string)text.Tag))
{
if (fDic[(string)text.Tag] != text.Text && !string.IsNullOrWhiteSpace(text.Text))
fDic[(string)text.Tag] = text.Text;
}
else if(!string.IsNullOrWhiteSpace(text.Text) != null)
{
fDic[(string)text.Tag] = text.Text;
}
else if (string.IsNullOrWhiteSpace(text.Text)
fDic.Remove((string)text.Tag);
}
}
In the code above, it will also remove from the dictionary if the text is empty.
EDIT
After some chat, we came with a better option to accomplish what you want to do.
First, make the dictionary a Form variable, like in the option above.
private Dictionary<string, string> fDic;
Then, when you populate the TextBox's, put one entry (with the TAG) for each text box in the fDic.
To do this, you can use this kind of code:
foreach(Control ctrl in MainGrid.Children)
{
TextBox tb = ctrl as TextBox;
if (tb != null)
fDic[(string)text.Tag] = text.Text;
}
When the buttom click event is called, just compare the text in the text boxes with the one's previously stored in the dic, using the dictionary to do this.
private void Submit_Button_Click(object sender, RoutedEventArgs e)
{
Dictionary<string, string> tagDict = new Dictionary<string, string>();
foreach(Control ctrl in MainGrid.Children)
{
TextBox tb = ctrl as TextBox;
if (tb != null)
{
if (fDic[(string)text.Tag] != text.Text)
{
string tagString = tb.Tag.ToString();
string textString = tb.Text;
tagDict.Add(tagString, textString);
}
}
}
}

WPF: Dropdown of a Combobox highlightes the text

When I type in the combobox I automatically opens enables the dropdown list
searchComboBox.IsDropDownOpen = true;
The problem here is - the text gets highlighted and the next keystrock overwrites the previous text.
How can I disable the text highlighting when ComboBox DropDown opens up?
I had this very same issue and like some of the users being new to WPF, struggled to get the solution given by Einar Guðsteinsson to work. So in support of his answer I'm pasting here the steps to get this to work. (Or more accurately how I got this to work).
First create a custom combobox class which inherits from the Combobox class. See code below for full implementation. You can change the code in OnDropSelectionChanged to suit your individual requirements.
namespace MyCustomComboBoxApp
{
using System.Windows.Controls;
public class MyCustomComboBox : ComboBox
{
private int caretPosition;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
var element = GetTemplateChild("PART_EditableTextBox");
if (element != null)
{
var textBox = (TextBox)element;
textBox.SelectionChanged += OnDropSelectionChanged;
}
}
private void OnDropSelectionChanged(object sender, System.Windows.RoutedEventArgs e)
{
TextBox txt = (TextBox)sender;
if (base.IsDropDownOpen && txt.SelectionLength > 0)
{
caretPosition = txt.SelectionLength; // caretPosition must be set to TextBox's SelectionLength
txt.CaretIndex = caretPosition;
}
if (txt.SelectionLength == 0 && txt.CaretIndex != 0)
{
caretPosition = txt.CaretIndex;
}
}
}
Ensure that this custom combo class exists in the same project. THen you can use the code below to reference this combo in your UI.
<Window x:Class="MyCustomComboBoxApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cc="clr-namespace:MyCustomComboBoxApp"
Title="MainWindow" Height="350" Width="525" FocusManager.FocusedElement="{Binding ElementName=cb}">
<Grid>
<StackPanel Orientation="Vertical">
<cc:MyCustomComboBox x:Name="cb" IsEditable="True" Height="20" Margin="10" IsTextSearchEnabled="False" KeyUp="cb_KeyUp">
<ComboBoxItem>Toyota</ComboBoxItem>
<ComboBoxItem>Honda</ComboBoxItem>
<ComboBoxItem>Suzuki</ComboBoxItem>
<ComboBoxItem>Vauxhall</ComboBoxItem>
</cc:MyCustomComboBox>
</StackPanel>
</Grid>
</Window>
Thats it! Any questions, please ask! I'll do my best to help.
THanks to Einar Guðsteinsson for his solution!
Better late than never and if some one else hit this proplem he might use this.
There is away todo this if you override combobox.
First get handle on the textbox that is used in the template and register to selectionchanged event.
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
var element = GetTemplateChild("PART_EditableTextBox");
if (element != null)
{
var textBox = (TextBox)element;
textBox.SelectionChanged += OnDropSelectionChanged;
}
}
private void OnDropSelectionChanged(object sender, RoutedEventArgs e)
{
// Your code
}
Then in the event handler you can set the selection again like you want it to be. In my case I was calling IsDropDownOpen in code. Saved selection there then put it back in the event handler. Ugly but did the trick.
Further to clsturgeon's answer, I have solved the issue by setting the selection when DropDownOpened event occurred:
private void ItemCBox_DropDownOpened(object sender, EventArgs e)
{
TextBox textBox = (TextBox)((ComboBox)sender).Template.FindName("PART_EditableTextBox", (ComboBox)sender);
textBox.SelectionStart = ((ComboBox)sender).Text.Length;
textBox.SelectionLength = 0;
}
I think in the Solution provided by Andrew N there is something missing as when I tried it out the Selection Changed event of the TextBox was placing the caret at the wrong place. So I made this change to solve that.
namespace MyCustomComboBoxApp { using System.Windows.Controls;
public class MyCustomComboBox : ComboBox
{
private int caretPosition;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
var element = GetTemplateChild("PART_EditableTextBox");
if (element != null)
{
var textBox = (TextBox)element;
textBox.SelectionChanged += OnDropSelectionChanged;
}
}
private void OnDropSelectionChanged(object sender, System.Windows.RoutedEventArgs e)
{
TextBox txt = (TextBox)sender;
if (base.IsDropDownOpen && txt.SelectionLength > 0)
{
caretPosition = txt.SelectionLength; // caretPosition must be set to TextBox's SelectionLength
txt.CaretIndex = caretPosition;
}
if (txt.SelectionLength == 0 && txt.CaretIndex != 0)
{
caretPosition = txt.CaretIndex;
}
}
}
When a comboxbox gains focus you can disable the text highlighting (i.e. by selecting no text upon the GotFocus event). However, when you pulldown the combobox the system is going to locate the item in the list and make that the selected item. This in turn automatically highlights the text. If I understand the behaviour you are looking for, I do not believe it is fully possible.
I was able to fix it using a modified answer from Jun Xie. Assuming you are using the keyUp event for your combobox search, I found an edge case in my custom use case that would still overwrite the text:
Type in the combobox for the first time. Text is fine.
Use up and down arrow keys to select an item in the list, but not "committing" the change (pressing enter, for example, and closing the dropDown selections. Note the text is highlighted at this point like clsturgeon points out.
Try to type in the textbox again. In this case the text will be over-ridden because the dropdown was still open hence the event never fired to clear the highlight.
The solution is to check if an item is selected. Here's the working code:
XAML:
<ComboBox x:Name="SearchBox" IsEditable="True" KeyUp="SearchBox_KeyUp"
PreviewMouseDown="SearchBox_PreviewMouseDown" IsTextSearchEnabled="False"
DropDownOpened="SearchBox_DropDownOpened">
</ComboBox>
Code:
private void SearchBox_KeyUp(object sender, KeyEventArgs e)
{
SearchBox.IsDropDownOpen = true;
if (e.Key == Key.Down || e.Key == Key.Up)
{
e.Handled = true;
//if trying to navigate but there's noting selected, then select one
if(SearchBox.Items.Count > 0 && SearchBox.SelectedIndex == -1)
{
SearchBox.SelectedIndex = 0;
}
}
else if (e.Key == Key.Enter)
{
//commit to selection
}
else if (string.IsNullOrWhiteSpace(SearchBox.Text))
{
SearchBox.Items.Clear();
SearchBox.IsDropDownOpen = false;
SearchBox.SelectedIndex = -1;
}
else if (SearchBox.Text.Length > 1)
{
//if something is currently selected, then changing the selected index later will loose
//focus on textbox part of combobox and cause the text to
//highlight in the middle of typing. this will "eat" the first letter or two of the user's search
if(SearchBox.SelectedIndex != -1)
{
TextBox textBox = (TextBox)((ComboBox)sender).Template.FindName("PART_EditableTextBox", (ComboBox)sender);
//backup what the user was typing
string temp = SearchBox.Text;
//set the selected index to nothing. sets focus to dropdown
SearchBox.SelectedIndex = -1;
//restore the text. sets focus and highlights the combobox text
SearchBox.Text = temp;
//set the selection to the end (remove selection)
textBox.SelectionStart = ((ComboBox)sender).Text.Length;
textBox.SelectionLength = 0;
}
//your search logic
}
}
private void SearchBox_DropDownOpened(object sender, EventArgs e)
{
TextBox textBox = (TextBox)((ComboBox)sender).Template.FindName("PART_EditableTextBox", (ComboBox)sender);
textBox.SelectionStart = ((ComboBox)sender).Text.Length;
textBox.SelectionLength = 0;
}
An alternative. Prevent the framework from messing with selection.
public class ReflectionPreventSelectAllOnDropDown
{
private static readonly PropertyInfo edtbPropertyInfo;
static ReflectionPreventSelectAllOnDropDown()
{
edtbPropertyInfo = typeof(ComboBox).GetProperty("EditableTextBoxSite", BindingFlags.NonPublic | BindingFlags.Instance);
}
public void DropDown(ComboBox comboBox)
{
if (!comboBox.IsDropDownOpen)
{
var edtb = edtbPropertyInfo.GetValue(comboBox);
edtbPropertyInfo.SetValue(comboBox, null);
comboBox.IsDropDownOpen = true;
edtbPropertyInfo.SetValue(comboBox, edtb);
}
}
}

Categories