I have a TabControl that displays default information for objects that can be selected by the user in a list.
Unfortunately not all of the TabItems apply to all types of objects. That's why I decided to use DataTriggers to hide some of them in such cases. When testing however I noticed that when the a TabItem is already selected while being set to collapsed then only the header vanishes, but the content of the TabItems stays visible.
When searching for a solution I only found this very old thread here:
WPF - TabItem Contents still visible when tabitem.visibility=hidden
I was wondering if there was is nicer solution today. The only thing that I could think of would be a custom TabControl that would look somehow like this:
public class MyTabControl : System.Windows.Controls.TabControl
{
public MyTabControl() : base()
{
var view = CollectionViewSource.GetDefaultView(this.Items);
view.CollectionChanged += TabControl_CollectionChanged;
}
private void TabControl_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (TabItem tabItem in e.NewItems)
{
tabItem.IsVisibleChanged += TabItem_IsVisibleChanged;
}
}
else if (this.Items != null)
{
foreach (TabItem tabItem in this.Items)
{
tabItem.IsVisibleChanged += TabItem_IsVisibleChanged;
}
}
}
private void TabItem_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
TabItem tabItem = sender as TabItem;
if (tabItem != null && tabItem.IsSelected && tabItem.Visibility != Visibility.Visible)
{
this.SelectedIndex = 0;
}
}
}
However this means that I have to use my own TabControl over the default one. Can anyone think of an even nicer solution?
Related
On my Window I have TreeView. TreeView.ItemsSource is binded to my data (hierarchical data structure). When I expand some TreeViewItem that is on the bottom of the current scrollbar position, my subitems are hidden and I have to scroll to see them. Is there any way to bring all subitems of expanded item into view?
I have tried this, but didn't work:
public void TreeViewItem_Expanded(object sender, RoutedEventArgs e)
{
TreeViewItem tvi = e.OriginalSource as TreeViewItem;
if (tvi != null)
{
Debug.WriteLine("TreeNode '{0}' was expanded", tvi.Header);
tvi.BringIntoView();
}
}
Basically, select the last sub-item and bring it into view when an item is expanded. If it has no subitems, bring the item itself into view.
The tricky part is, subitems are not necessarily available when the event is executed, so I propose you check the ItemContainerGenerator.Status and if it's not yet done, subscribe to the ItemContainerGenerator.StatusChanged event.
The following code is only tested with static items, if it's making problem for dynamic data sources, I may have to re-check a few details.
void TreeViewItem_Expanded(object sender, RoutedEventArgs e)
{
TreeViewItem tvi = e.OriginalSource as TreeViewItem;
if (tvi != null)
{
if (tvi.HasItems)
{
if (tvi.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
BringLastItemIntoView(tvi.ItemContainerGenerator);
}
else
{
tvi.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
}
}
else
{
tvi.BringIntoView();
}
}
}
void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
var s = sender as ItemContainerGenerator;
if (s.Status == GeneratorStatus.ContainersGenerated)
{
BringLastItemIntoView(s);
s.StatusChanged -= ItemContainerGenerator_StatusChanged;
}
}
void BringLastItemIntoView(ItemContainerGenerator generator)
{
var tvi = generator.ContainerFromItem(generator.Items.LastOrDefault()) as TreeViewItem;
if (tvi != null)
{
tvi.BringIntoView();
}
}
I've got two ItemsControls, which use the same collection as an ItemsSource. ItemTemplates for both ItemsControls are very similar: there is a ScrollViewer inside both DataTemplates.
Question:
If I change HorizontalOffset in one of the ScrollViewers generated in the first ItemsControl, how can I scroll the ScrollViewer from the second ItemsControl, which has the same object as a DataContext, to the same HorizontalOffset?
If you do a Google search for WPF ScrollViewer Synchronization you'll get a lot of good results, such as this codeproject article
As Rachel mentioned, there may be lots of solutions about your problem. However, the things that I have seen are a little complex or not simple for me. So, I thought this way, which is very simple and can apply all the control which has ScrollViewer in VisualTree.
The following code assumes that the two controls, having ScrollViewer in VisualTree, are already defined in Xaml, which are named control1 and control2 in my code.
public partial class MainWindow : Window
{
private ScrollViewer _scrollViewer1;
private ScrollViewer _scrollViewer2;
public MainWindow()
{
InitializeComponent();
Loaded += (s, e) =>
{
_scrollViewer1 = FindScrollViewer(**control1**);
_scrollViewer2 = FindScrollViewer(**control2**);
if (_scrollViewer1 == null || _scrollViewer2 == null) throw new InvalidOperationException();
_scrollViewer1.ScrollChanged += ScrollViewer1ScrollChanged;
_scrollViewer2.ScrollChanged += ScrollViewer2ScrollChanged;
};
}
private ScrollViewer FindScrollViewer(Visual visual)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(visual); i++)
{
var visualChild = VisualTreeHelper.GetChild(visual, i) as Visual;
if (visualChild == null) continue;
var scrollViewer = visualChild as ScrollViewer;
if (scrollViewer != null)
{
return scrollViewer;
}
return FindScrollViewer(visualChild);
}
return null;
}
private void ScrollViewer1ScrollChanged(object sender, ScrollChangedEventArgs e)
{
_scrollViewer2.ScrollToVerticalOffset(_scrollViewer1.VerticalOffset);
}
private void ScrollViewer2ScrollChanged(object sender, ScrollChangedEventArgs e)
{
_scrollViewer1.ScrollToVerticalOffset(_scrollViewer2.VerticalOffset);
}
}
I need to find the ComboBox that a ComboBoxItem resides in.
In codebehind I catch an event when a ComboBoxItem is clicked, but I don't know which one of several ComboBoxes that the specific ComboBoxItem belongs to. How do I find the ComboBox?
Normally you can use LogicalTreeHelper.GetParent() and traverse up the logical tree from the ComboBoxItem to find the ComboBox. But this only works if the ComboBoxItems are added to the ComboBox manually, not when the items are applied to the ComboBox with databinding. When using databinding, the ComboBoxItems do not have the ComboBox as a logical parent (I don't understand why).
Any ideas?
More info:
Below is some code reconstructing my problem (not my actual code). If I would change from databinding the ComboBoxItems to setting them manually (in the XAML), the variable "comboBox" would be set to the correct ComboBox. Now comboBox is only null.
XAML:
<ComboBox Name="MyComboBox" ItemsSource="{Binding Path=ComboBoxItems, Mode=OneTime}" />
CodeBehind:
public MainWindow()
{
InitializeComponent();
MyComboBox.DataContext = this;
this.PreviewMouseDown += MainWindow_MouseDown;
}
public BindingList<string> ComboBoxItems
{
get
{
BindingList<string> items = new BindingList<string>();
items.Add("Item E");
items.Add("Item F");
items.Add("Item G");
items.Add("Item H");
return items;
}
}
private void MainWindow_MouseDown(object sender, MouseButtonEventArgs e)
{
DependencyObject clickedObject = e.OriginalSource as DependencyObject;
ComboBoxItem comboBoxItem = FindVisualParent<ComboBoxItem>(clickedObject);
if (comboBoxItem != null)
{
ComboBox comboBox = FindLogicalParent<ComboBox>(comboBoxItem);
}
}
//Tries to find visual parent of the specified type.
private static T FindVisualParent<T>(DependencyObject childElement) where T : DependencyObject
{
DependencyObject parent = VisualTreeHelper.GetParent(childElement);
T parentAsT = parent as T;
if (parent == null)
{
return null;
}
else if (parentAsT != null)
{
return parentAsT;
}
return FindVisualParent<T>(parent);
}
//Tries to find logical parent of the specified type.
private static T FindLogicalParent<T>(DependencyObject childElement) where T : DependencyObject
{
DependencyObject parent = LogicalTreeHelper.GetParent(childElement);
T parentAsT = parent as T;
if (parent == null)
{
return null;
}
else if(parentAsT != null)
{
return parentAsT;
}
return FindLogicalParent<T>(parent);
}
This is probably what you are looking for:
var comboBox = ItemsControl.ItemsControlFromItemContainer(comboBoxItem) as ComboBox;
I love how descriptive that method-name is.
On a side-note, there are some other useful methods which can be found in the property ItemsControl.ItemContainerGenerator which let you get the container associated with the templated data and vice versa.
On another side-note, you usually should not be using any of them and instead use data-binding actually.
In wpf I setup a tab control that binds to a collection of objects each object has a data template with a data grid presenting the data. If I select a particular cell and put it into edit mode, leaving the grid by going to another tab this will cause the exception below to be thrown on returning the datagrid:
'DeferRefresh' is not allowed during an AddNew or EditItem transaction.
It appears that the cell never left edit mode. Is there an easy way to take the cell out of edit mode, or is something else going on here?
Update: It looks like if I do not bind the tab control to the data source, but instead explicitly define each tab and then bind each item in the data source to a content control this problem goes away. This is not really a great solution, so I would still like to know how to bind the collection directly to the tab control.
Update: So what I have actually done for my own solution is to use a ListView and a content control in place of a tab control. I use a style to make the list view look tab like. The view model exposes a set of child view models and allows the user to select one via the list view. The content control then presents the selected view model and each view model has an associated data template which contains the data grid. With this setup switching between view models while in edit mode on the grid will properly end edit mode and save the data.
Here is the xaml for setting this up:
<ListView ItemTemplate="{StaticResource MakeItemsLookLikeTabs}"
ItemsSource="{Binding ViewModels}"
SelectedItem="{Binding Selected}"
Style="{StaticResource MakeItLookLikeATabControl}"/>
<ContentControl Content="{Binding Selected}">
I'll accept Phil's answer as that should work also, but for me the solution above seems like it will be more portable between projects.
I implemented a behavior for the DataGrid based on code I found in this thread.
Usage:<DataGrid local:DataGridCommitEditBehavior.CommitOnLostFocus="True" />
Code:
using System.Collections.Generic;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
/// <summary>
/// Provides an ugly hack to prevent a bug in the data grid.
/// https://connect.microsoft.com/VisualStudio/feedback/details/532494/wpf-datagrid-and-tabcontrol-deferrefresh-exception
/// </summary>
public class DataGridCommitEditBehavior
{
public static readonly DependencyProperty CommitOnLostFocusProperty =
DependencyProperty.RegisterAttached(
"CommitOnLostFocus",
typeof(bool),
typeof(DataGridCommitEditBehavior),
new UIPropertyMetadata(false, OnCommitOnLostFocusChanged));
/// <summary>
/// A hack to find the data grid in the event handler of the tab control.
/// </summary>
private static readonly Dictionary<TabPanel, DataGrid> ControlMap = new Dictionary<TabPanel, DataGrid>();
public static bool GetCommitOnLostFocus(DataGrid datagrid)
{
return (bool)datagrid.GetValue(CommitOnLostFocusProperty);
}
public static void SetCommitOnLostFocus(DataGrid datagrid, bool value)
{
datagrid.SetValue(CommitOnLostFocusProperty, value);
}
private static void CommitEdit(DataGrid dataGrid)
{
dataGrid.CommitEdit(DataGridEditingUnit.Cell, true);
dataGrid.CommitEdit(DataGridEditingUnit.Row, true);
}
private static DataGrid GetParentDatagrid(UIElement element)
{
UIElement childElement; // element from which to start the tree navigation, looking for a Datagrid parent
if (element is ComboBoxItem)
{
// Since ComboBoxItem.Parent is null, we must pass through ItemsPresenter in order to get the parent ComboBox
var parentItemsPresenter = VisualTreeFinder.FindParentControl<ItemsPresenter>(element as ComboBoxItem);
var combobox = parentItemsPresenter.TemplatedParent as ComboBox;
childElement = combobox;
}
else
{
childElement = element;
}
var parentDatagrid = VisualTreeFinder.FindParentControl<DataGrid>(childElement);
return parentDatagrid;
}
private static TabPanel GetTabPanel(TabControl tabControl)
{
return
(TabPanel)
tabControl.GetType().InvokeMember(
"ItemsHost",
BindingFlags.NonPublic | BindingFlags.GetProperty | BindingFlags.Instance,
null,
tabControl,
null);
}
private static void OnCommitOnLostFocusChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
var dataGrid = depObj as DataGrid;
if (dataGrid == null)
{
return;
}
if (e.NewValue is bool == false)
{
return;
}
var parentTabControl = VisualTreeFinder.FindParentControl<TabControl>(dataGrid);
var tabPanel = GetTabPanel(parentTabControl);
if (tabPanel != null)
{
ControlMap[tabPanel] = dataGrid;
}
if ((bool)e.NewValue)
{
// Attach event handlers
if (parentTabControl != null)
{
tabPanel.PreviewMouseLeftButtonDown += OnParentTabControlPreviewMouseLeftButtonDown;
}
dataGrid.LostKeyboardFocus += OnDataGridLostFocus;
dataGrid.DataContextChanged += OnDataGridDataContextChanged;
dataGrid.IsVisibleChanged += OnDataGridIsVisibleChanged;
}
else
{
// Detach event handlers
if (parentTabControl != null)
{
tabPanel.PreviewMouseLeftButtonDown -= OnParentTabControlPreviewMouseLeftButtonDown;
}
dataGrid.LostKeyboardFocus -= OnDataGridLostFocus;
dataGrid.DataContextChanged -= OnDataGridDataContextChanged;
dataGrid.IsVisibleChanged -= OnDataGridIsVisibleChanged;
}
}
private static void OnDataGridDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var dataGrid = (DataGrid)sender;
CommitEdit(dataGrid);
}
private static void OnDataGridIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var senderDatagrid = (DataGrid)sender;
if ((bool)e.NewValue == false)
{
CommitEdit(senderDatagrid);
}
}
private static void OnDataGridLostFocus(object sender, KeyboardFocusChangedEventArgs e)
{
var dataGrid = (DataGrid)sender;
var focusedElement = Keyboard.FocusedElement as UIElement;
if (focusedElement == null)
{
return;
}
var focusedDatagrid = GetParentDatagrid(focusedElement);
// Let's see if the new focused element is inside a datagrid
if (focusedDatagrid == dataGrid)
{
// If the new focused element is inside the same datagrid, then we don't need to do anything;
// this happens, for instance, when we enter in edit-mode: the DataGrid element loses keyboard-focus,
// which passes to the selected DataGridCell child
return;
}
CommitEdit(dataGrid);
}
private static void OnParentTabControlPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var dataGrid = ControlMap[(TabPanel)sender];
CommitEdit(dataGrid);
}
}
public static class VisualTreeFinder
{
/// <summary>
/// Find a specific parent object type in the visual tree
/// </summary>
public static T FindParentControl<T>(DependencyObject outerDepObj) where T : DependencyObject
{
var dObj = VisualTreeHelper.GetParent(outerDepObj);
if (dObj == null)
{
return null;
}
if (dObj is T)
{
return dObj as T;
}
while ((dObj = VisualTreeHelper.GetParent(dObj)) != null)
{
if (dObj is T)
{
return dObj as T;
}
}
return null;
}
}
I have managed to work around this issue by detecting when the user clicks on a TabItem and then committing edits on visible DataGrid in the TabControl. I'm assuming the user will expect their changes to still be there when they click back.
Code snippet:
// PreviewMouseDown event handler on the TabControl
private void TabControl_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (IsUnderTabHeader(e.OriginalSource as DependencyObject))
CommitTables(yourTabControl);
}
private bool IsUnderTabHeader(DependencyObject control)
{
if (control is TabItem)
return true;
DependencyObject parent = VisualTreeHelper.GetParent(control);
if (parent == null)
return false;
return IsUnderTabHeader(parent);
}
private void CommitTables(DependencyObject control)
{
if (control is DataGrid)
{
DataGrid grid = control as DataGrid;
grid.CommitEdit(DataGridEditingUnit.Row, true);
return;
}
int childrenCount = VisualTreeHelper.GetChildrenCount(control);
for (int childIndex = 0; childIndex < childrenCount; childIndex++)
CommitTables(VisualTreeHelper.GetChild(control, childIndex));
}
This is in the code behind.
This bug is solved in the .NET Framework 4.5. You can download it at this link.
What I think you should do is pretty close to what #myermian said.
There is an event called CellEditEnding end this event would allow you to intercept and make the decision to drop the unwanted row.
private void dataGrid1_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
DataGrid grid = (DataGrid)sender;
TextBox cell = (TextBox)e.EditingElement;
if(String.IsNullOrEmpty(cell.Text) && e.EditAction == DataGridEditAction.Commit)
{
grid.CancelEdit(DataGridEditingUnit.Row);
e.Cancel = true;
}
}
I've got a rather funny problem with WPF. I have a tree-view, and selecting items works fine so far. The problem is, I want to unselect the currently selected item when the user clicks inside the blank area of the treeview. By default, the treeview keeps the current item selected, and I have added a context-menu option to deselect it, which is rather hardcore:
// Note: This is done recursivly from the start, so it
// works for child items as well
treeView.ItemContainerGenerator.ContainerFromItem(treeView.SelectedItem) as TreeViewItem).IsSelected = false;
Moreover, this is counter-intuitive, as it requires the user to right-click first, and second, after deselecting it with this way, the user cannot select it any more by clicking on the item. How is this supposed to work?
Edit: Some more information: I've added a handler to the TreeView to handle mouse click events, but the sender is always a TreeView instance, even if I click directly on a TreeViewItem. If I add a handler to my TreeView.ItemTemplate instead (i.e. the first child in the template), I never get events when I click on the empty area (which is rather logical). The code looks like this:
private void MyTreeView_MouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if ((sender as TreeViewItem) == null)
{
// Always triggered
System.Diagnostics.Trace.Write("Empty area clicked");
}
}
And the XAML for this is:
<TreeView x:Name="MyTreeView" Margin="3" MouseUp="MyTreeView_MouseUp">
I found this to work much better for me. I check the originalsource which for me if it comes form a treeviewitem will be an image or a textblock. I also use a view object with a HierarchicalDataTemplate and the BasicTreeViewBase is the base class for all of my different objects. Here is the code.
private void TemplateTreeView_MouseDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Right && !(e.OriginalSource is Image) && !(e.OriginalSource is TextBlock))
{
BasicTreeViewBase item = TemplateTreeView.SelectedItem as BasicTreeViewBase;
if (item != null)
{
TemplateTreeView.Focus();
item.IsSelected = false;
}
}
}
The un-selectable problem can be solved with a call to Focus on the TreeView after setting TreeViewItem.IsSelected.
There can be two more problem :
The treeview is binded so the SelectedItem is an item of the binded collection.
There is many levels so ItemContainerGenerator do not contain deepest level objects
for all this reason i use this function, but the selection must not fire any events.
private void UnselectTreeViewItem(TreeView pTreeView)
{
if(pTreeView.SelectedItem == null)
return;
if(pTreeView.SelectedItem is TreeViewItem)
{
(pTreeView.SelectedItem as TreeViewItem).IsSelected = false;
}
else
{
TreeViewItem item = pTreeView.ItemContainerGenerator.ContainerFromIndex(0) as TreeViewItem;
if (item != null)
{
item.IsSelected = true;
item.IsSelected = false;
}
}
}
I implemented a general selection control once, and required this behaviour.
This is how my method looked (adapted for treeview):
protected override void OnMouseUp(MouseButtonEventArgs e)
{
base.OnMouseUp(e);
DependencyObject dpSource = e.OriginalSource as DependencyObject;
if (dpSource.FindVisualAncestor(o => typeof(TreeViewItem).IsAssignableFrom(o.GetType())) == null)
UnselectAll();
}
Basically, walk up the tree from the source. If a TreeViewItem was not found, than the user clicked empty space.
Use the extension class below
public static class TreeViewExtensions
{
public static TreeViewItem ContainerFromItem(this TreeView treeView, object item)
{
TreeViewItem containerThatMightContainItem = (TreeViewItem)treeView.ItemContainerGenerator.ContainerFromItem(item);
if (containerThatMightContainItem != null)
return containerThatMightContainItem;
else
return ContainerFromItem(treeView.ItemContainerGenerator, treeView.Items, item);
}
private static TreeViewItem ContainerFromItem(ItemContainerGenerator parentItemContainerGenerator, ItemCollection itemCollection, object item)
{
foreach (object curChildItem in itemCollection)
{
TreeViewItem parentContainer = (TreeViewItem)parentItemContainerGenerator.ContainerFromItem(curChildItem);
TreeViewItem containerThatMightContainItem = (TreeViewItem)parentContainer.ItemContainerGenerator.ContainerFromItem(item);
if (containerThatMightContainItem != null)
return containerThatMightContainItem;
TreeViewItem recursionResult = ContainerFromItem(parentContainer.ItemContainerGenerator, parentContainer.Items, item);
if (recursionResult != null)
return recursionResult;
}
return null;
}
}
Then in MouseDown event of treeview use the extension method as below:
private void trview_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if ((sender as TreeViewItem) == null)
{
if (this.trview.ContainerFromItem(trview.SelectedItem) != null)
{
this.trview.ContainerFromItem(trview.SelectedItem).IsSelected = false;
}
}
this.trview.Focus();
}
Hope it works for you. I have it working in this way...
I was running into this situation myself with a custom Tree List View implementation after looking for a long time I finally found a solution that worked for me.
The full explanation can be found at http://social.msdn.microsoft.com/Forums/vstudio/en-US/36aca7f7-0b47-488b-8e16-840b86addfa3/getting-treeviewitem-for-the-selected-item-in-a-treeview
The basic idea is you capture the TreeViewItem.Selected event and save the source of the event into the Tag attribute on your TreeView. Then when you need to clear it, you can access the Tag attribute on your control and set the IsSelected value to False. This works for me with 2 levels of nested children. Hopefully it will work for you.
For persistence sake:
TreeView declaration
<TreeView Name="myTreeView" TreeViewItem.Selected="OnItemSelected"
ItemsSource="{Binding Source={StaticResource myHierarchicalData}}"/>
Event Handler
private void OnItemSelected(object sender, RoutedEventArgs e)
{
myTreeView.Tag = e.OriginalSource;
}
Clear selection logic
if (myTreeView.SelectedItem != null)
{
TreeViewItem selectedTVI = myTreeView.Tag as TreeViewItem;
// add your code here mine was selectedTVI.IsSelected = false;
}
This will deselect the currently selected TreeViewItem if none were clicked:
private void MyTreeView_PreviewMouseDown(object sender, MouseButtonEventArgs e) {
if ((sender as TreeViewItem) == null) {
TreeViewItem item = MyTreeView.SelectedItem as TreeViewItem;
if(item != null){
item.IsSelected = false;
}
}
}
Hope this is what you were looking for!
MVVM: Call this method in the eventhandler righ/left mouse click:
private void SetTreeViewSelection(object sender, MouseButtonEventArgs mouseButtonEventArgs)
{
var treeView = (TreeView)sender;
if (treeView.SelectedItem == null)
{
return;
}
IInputElement dropNode = treeView.InputHitTest(mouseButtonEventArgs.GetPosition(treeView));
if (dropNode is ScrollViewer)
{
var myBindableObject = (MyBindableObject)treeView.SelectedItem;
myBindableObject.IsSelected = false;
}
}
For a C# treeview you use treeview.SelectedNode = null; I'm not sure if this works for WPF.