I have many forms with a lot of textboxes. And I want to add to that forms a button with IsDefault = true. Then fill any property and press enter. If I will not set UpdateSourceTrigger=PropertyChanged on textbox, it will not see my input.
The problem is that I do not want to add UpdateSourceTrigger=PropertyChanged to each textbox, combobox, checkbox, and etc.
Is there any way to trigger everything to write it's data to the source without adding UpdateSourceTrigger=PropertyChanged?
Call the BindingExpression.UpdateSource method in the click event handler for each of the Controls (TextBox, CheckBox, etc.) that you have in your Window/UserControl, like this:
private void ButtonBase_OnClick(object sender, RoutedEventArgs e) {
this.UpdateSourceTrigger(this);
MessageBox.Show($"Field1: {this.vm.Field1}\nField2: {this.vm.Field2}\nField3: {this.vm.Field3}");
}
public void UpdateSourceTrigger(UIElement element) {
if (element == null) return;
var children = LogicalTreeHelper.GetChildren(element);
foreach (var e in children) {
if (e is TextBox txtBox) {
var binding = txtBox.GetBindingExpression(TextBox.TextProperty);
binding?.UpdateSource();
}
if (e is CheckBox chkBox) {
var binding = chkBox.GetBindingExpression(CheckBox.IsCheckedProperty);
binding?.UpdateSource();
}
// add other types, like ComboBox or others...
// ...
this.UpdateSourceTrigger(e as UIElement);
}
}
Related
I have a piece of code that will add the selected item of a combobox to a listbox when a checkbox is checked. I would like to remove that selected item to be removed from the listbox when the checkbox is unchecked.
My problem is I cant simply repeat the code for removal to be the same as add because the combobox selection will different or empty when unchecked.
This is how it currently looks:
private void CBwasher_CheckedChanged(object sender, EventArgs e)
{
if (checkBox1.Checked == true)
{
listBox2.Items.Add(comboBox1.SelectedItem);
}
if (checkBox1.Checked == false)
{
listBox2.Items.Remove(comboBox1.SelectedItem);
}
So I need a way of removing whatever was added by that check change instead of removing the selected index of the combobox. Please consider that there may be multiple lines in the listbox added by multiple different checkboxes.
You simply need to store the added item and remove it.
private object addedItem;
private void CBwasher_CheckedChanged(object sender, EventArgs e)
{
if (checkBox1.Checked)
{
addedItem = comboBox1.SelectedItem;
listBox2.Items.Add(addedItem);
}
else
{
listBox2.Items.Remove(addedItem);
}
}
You may also need to check SelectedItem is null before adding/removing the item.
Focusing on the part where you said there might be multiple different checkboxes,
you need to store one item per checkbox.
You can write your own child class of the checbox control to add this feature, or simply use the Tag property.
You can also indicate which checkbox is linked to which combobox in the same way. Either child class or use the Tag property.
In my example, I'll assume you've referenced the combobox from the checkbox using the Tag property.
You can do it manually like this
checkBox1.Tag = comboBox1;
or hopefully you can automate it if you are generating these on the fly.
Here is the general idea of how the checkbox event should look.
The event is is utilising the sender argument, which means you should hook up all your checkboxes CheckedChanged events to this one handler. No need to create separate handlers for each.
private void CBwasher_CheckedChanged(object sender, EventArgs e)
{
var checkBox = (CheckBox)sender;
var comboBox = (ComboBox)checkBox.Tag;
if (checkBox.Checked && comboBox.SelectedItem != null)
{
listBox2.Items.Add(comboBox.SelectedItem);
comboBox.Tag = comboBox.SelectedItem;
}
if (!checkBox.Checked && comboBox.Tag != null)
{
listBox2.Items.Remove(comboBox.Tag);
}
}
Is there a way to capture an attempt to change currently selected item in the WPF's TreeView and possibly cancel it?
Elements in the treeview represent pages with some properties. I would like to ask user if he wants to abandon changes made on the page, save them or stay in the current page.
For a much simpler solution. Override the PreviewMouseDown and you will get the desired result.
private void Tree_OnPreviewMouseDown(object sender, MouseButtonEventArgs e)
{
// first did the user click on a tree node?
var source = e.OriginalSource as DependencyObject;
while (source != null && !(source is TreeViewItem))
source = VisualTreeHelper.GetParent(source);
source = source as TreeViewItem;
if (source == null) return;
var treeView = sender as TreeView;
if (treeView == null) return;
// validate our current item to decide if we allow the change
// or do whatever checks you wish
if (!ItemIsValid(treeView.SelectedItem))
{
// it's not valid, so cancel the attempt to select an item.
e.Handled = true;
}
// Maybe you want to check the about to be selected value?
MyClass data = source.DataContext;
if (!CanSelect(data))
{
// we can't select this, so cancel the attempt to select.
e.Handled = true;
}
}
Well you're probably not going to like the answer... the WPF TreeView is an unfriendly fellow. Ok, first things first...
capturing an attempt to change the selected item:
The easiest way to do this is to handle the SelectedItemChanged event:
private void TreeView_SelectedItemChanged(object sender,
RoutedPropertyChangedEventArgs<object> e)
{
e.Handled = true;
}
Unfortunately, if you're using MVVM, then you'll need to handle this inside an Attached Property. Getting a bit more complicated now, if you're going to create an Attached Property to handle the SelectedItemChanged event, then you might as well implement a SelectedItem Attached Property that you could bind to in Two-Way Mode. I won't document how to do this here because there are plenty of online tutorials for this.
... and possibly cancel it:
If you have a SelectedItem Attached Property, then you can monitor when that property changes. There is a catch of course... by the time the change comes into your view model, the UI has already changed. So, although you can stop the change from happening to the data in the view model, you cannot stop the selection being made in the UI.
This is not a terrible problem though, because with a Two-Way Binding, you will be able to set the UI selection back to the previous item if necessary... take a look at this pseudo code:
public YourDataType SelectedItem
{
get { return selectedItem; }
set
{
if (selectedItem != value)
{
if (selectedItem.HasChanges)
{
if (WindowManager.UserAcceptsLoss())
{
selectedItem = value;
NotifyPropertyChanged("SelectedItem");
}
else ResetSelectedItem(selectedItem);
}
else
{
selectedItem = value;
NotifyPropertyChanged("SelectedItem");
}
}
}
}
To fulfil your requirements, you have a lot of work ahead... good luck with that.
Using PreviewMouseDown is half-solution. User may change selection from keyboard. Use TreeViewItem.Selected in all tree items:
private TreeViewItem _currenеTreeViewItem;
private bool _treeViewItemSelectedWork;
private void TreeViewItem_Selected(object sender, RoutedEventArgs e)
{
if(_treeViewItemSelectedWork)
return;
_treeViewItemSelectedWork = true;
TreeViewItem newTreeViewItem = (TreeViewItem) sender;
if (newTreeViewItem.IsSelected)
{
if (_currenеTreeViewItem == null)
_currenеTreeViewItem = newTreeViewItem;
if (!Equals(newTreeViewItem, _currenеTreeViewItem))
{
if (MessageBox.Show("Undo into previous selection?", "Undo", MessageBoxButton.YesNo,
MessageBoxImage.None) == MessageBoxResult.Yes)
{
newTreeViewItem.IsSelected = false;
_currenеTreeViewItem.IsSelected = true;
}
else
{
_currenеTreeViewItem.IsSelected = false;
_currenеTreeViewItem = newTreeViewItem;
_currenеTreeViewItem.IsSelected = true;
}
}
}
_treeViewItemSelectedWork = false;
}
I subscribed to a SelectionChangedEvent on a ComboBox in a DataGrid with the following code:
public static DataGridTemplateColumn CreateComboboxColumn(string colName, Binding textBinding, SelectionChangedEventHandler selChangedHandler = null)
{
var cboColumn = new DataGridTemplateColumn {Header = colName};
...
if (selChangedHandler != null)
cboFactory.AddHandler(Selector.SelectionChangedEvent, selChangedHandler);
...
return cboColumn;
}
The handler I actually register contains:
private void ComboBoxSelectionChangedHandler(object sender, SelectionChangedEventArgs e)
{
Console.WriteLine(#"selectHandler");
var cboBox = sender as ComboBox;
if (cboBox == null)
return;
if (cboBox.IsDropDownOpen) // a selection in combobox was made
{
CommitEdit();
}
else // trigger the combobox to show its list
cboBox.IsDropDownOpen = true;
}
... and is located in my custom DataGrid class.
If I select an item in the ComboBox, e.AddedItems and cboBox.SelectedItem contains the selected value, but nothing is changed on CommitEdit().
What I want is to force a commit to directly update the DataGrid's ItemsSource, when the user selects an item in the drop-down-list. Normally this is raised if the control looses focus...
The link in the solution found in this thread is not available any more and I don't know how to use this code.
I created a tricky, but working, solution for my problem. Here's the modified handler from above:
private void ComboBoxSelectionChangedHandler(object sender, SelectionChangedEventArgs e)
{
Console.WriteLine(#"selectHandler");
var cboBox = sender as ComboBox;
if (cboBox == null)
return;
if (cboBox.IsDropDownOpen) // a selection in combobox was made
{
cboBox.Text = cboBox.SelectedValue as string;
cboBox.MoveFocus(new TraversalRequest(FocusNavigationDirection.Right));
}
else // user wants to open the combobox
cboBox.IsDropDownOpen = true;
}
Because my ComboBoxColumn is a custom DataGridTemplateColumn I force it to show its list, when the user first selects the cell.
To change the bound items value I manually overwrite the displayed text with recently selected item and force the UI to select another item (in this case the control to the right) to make an implicit call to CellEditEnding event, which (in my case) commits the whole row:
private bool _isManualEditCommit = false;
private void _CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
// commit a manual edit
// this if-clause prevents double execution of the EditEnding event
if (!_isManualEditCommit)
{
Console.WriteLine(#"_CellEditEnding() manualeditcommit");
_isManualEditCommit = true;
CommitEdit(DataGridEditingUnit.Row, true);
_isManualEditCommit = false;
checkRow(e.Row);
}
}
Maybe I could help somebody with this "dirty" solution ;-)
I have a telerik grid view and when I right click the header, I'm showing a with a ListBox inside containing the list of columns.
The item template is redefined to show a check box so I can set the column visible or not.
I can also drag/drop columns to reorder them.
So here is how I create my Popup:
var view = new ColumnsOrderer.ColumnsOrderer
{
DataContext = new ColumnsOrderer.ViewModelColumnsOrderer(Columns)
};
var codePopup = new Popup
{
Child = view,
MaxHeight = 400,
StaysOpen = false,
Placement = PlacementMode.Mouse
};
codePopup.IsOpen = true;
Now everything seems to work correctly, but it's not.
If I set columns visible or hidden and then click outside the popup, it closes correctly.
Though if I drag an item to reorder it, the popup seems to lose focus and then it won't close if I click outside the popup. I have to click back in the list box inside the popup and then it closes by clicking outside.
Here is my drag/drop events:
public ColumnsOrderer()
{
InitializeComponent();
InitialiazeListBoxDragDrop();
}
private void InitialiazeListBoxDragDrop()
{
var itemContainerStyle = new Style(typeof(ListBoxItem));
itemContainerStyle.Setters.Add(new Setter(AllowDropProperty, true));
itemContainerStyle.Setters.Add(new EventSetter(PreviewMouseMoveEvent, new MouseEventHandler(OnMouseMove)));
itemContainerStyle.Setters.Add(new EventSetter(DropEvent, new DragEventHandler(OnDrop)));
listColumns.ItemContainerStyle = itemContainerStyle;
}
void OnMouseMove(object sender, MouseEventArgs e)
{
if (e.OriginalSource is CheckBox || e.LeftButton == MouseButtonState.Released)
return;
if (sender is ListBoxItem)
{
var draggedItem = sender as ListBoxItem;
draggedItem.IsSelected = true;
DragDrop.DoDragDrop(draggedItem, draggedItem.DataContext, DragDropEffects.Move);
}
}
void OnDrop(object sender, DragEventArgs e)
{
if (!(sender is ListBoxItem))
return;
}
An interesting thing is that if I remove the OnDrop handler, the problem is not there.
I tried many ways to set back the focus to the popup, but it's not working.
Could anyone help me on that?
How about trying to re-focus your Popup control after the drag and drop operation?
void OnDrop(object sender, DragEventArgs e)
{
if (!(sender is ListBoxItem))
return;
codePopup.Focus();
}
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);
}
}
}