I am trying to bind my MouseDoubleClick event to whenever the user double clicks only on a day in the calendar, which will open a new window for that day. However the latter is performed and fetches the highlighted day even if the user double clicks anywhere else in the calendar area.
I tried to do it using the style option however I am getting the same result as if I place it in the calendar definition line:
<Calendar x:Name="calendar" Grid.Column="1" HorizontalAlignment="Stretch"
Margin="10,7,0,0" VerticalAlignment="Top" IsTodayHighlighted="True"
MouseDoubleClick="event">
Same Result as
<Style TargetType="CalendarDayButton">
<EventSetter Event="MouseDoubleClick" Handler="Cdb_MouseDoubleClick"/>
</Style>
How can I differentiate between when a day is press, when the month is pressed, when nothing is pressed, instead of what is focused?
EDIT (this method is working using xaml):
<Calendar x:Name="calendar" Grid.Column="1" HorizontalAlignment="Stretch"
Margin="10,7,0,0" VerticalAlignment="Top"
IsTodayHighlighted="True" SelectionMode="SingleDate">
<Calendar.CalendarDayButtonStyle>
<Style TargetType="CalendarDayButton">
<EventSetter Event="MouseDoubleClick" Handler="CalendarDayButton_MouseDoubleClick"/>
</Style>
</Calendar.CalendarDayButtonStyle>
</Calendar>
private void CalendarDayButton_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
MessageBox.Show("Test");
}
For something like this, I generally look at e.OriginalSource, then walk up the visual tree to find the target parent type, in your case CalendarDayButton. The original source is generally a TextBlock or some primitive, as that is what user actually clicks on. Also, there is no need for applying a style to CalendarDayButton.
So if you put the double click event handler on your Calendar as per your first line of code, you can do it like below. There, if a visual parent of the is not found, FindParentOfType() method will return null. Then it is just a matter of testing for null. If not null, means you have the correct target.
<Calendar x:Name="calendar" Grid.Column="1" HorizontalAlignment="Stretch"
Margin="10,7,0,0" VerticalAlignment="Top" IsTodayHighlighted="True"
MouseDoubleClick="calendar_MouseDoubleClick">
private void calendar_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
DependencyObject originalSource = e.OriginalSource as DependencyObject;
CalendarDayButton day = FindParentOfType<CalendarDayButton>(originalSource);
if (day != null)
{
//open menu
}
e.Handled = true;
}
//and you will need this helper method
//generally a staple in any WPF programmer's arsenal
public static T FindParentOfType<T>(DependencyObject source) where T : DependencyObject
{
T ret = default(T);
DependencyObject parent = VisualTreeHelper.GetParent(source);
if (parent != null)
{
ret = parent as T ?? FindParentOfType<T>(parent) as T;
}
return ret;
}
Related
I have a list of devices and in that list user will select which COM port represents which device, each device has its own StackPanel shown below:
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Width="140" Text="IMT" VerticalAlignment="Center"/>
<ComboBox Width="250" Margin="0,0,40,0" x:Name="FM_list" SelectionChanged="DeviceSelected"/>
<TextBlock x:Name="FM_selection" Margin="0,0,40,0" Width="80 "/>
<Button Background="Red" Width="50" Click="Port_selected" x:Name="FM_selection1"/>
</StackPanel>
After user makes his selection in the ComboBox it is verified by clicking an adjecent Button.
I'd like it so that when the Button is clicked x:Name of the TextBlock (or an alternate way of referencing) is passed to the Port_selected function so I can get the correct device when calling TextBox.Name on the sender.
I could a seperate x:Name for each of those buttons and a dictionary to match which button matches which TextBlock and which StackPanel, but I'd like to know how to do without that workaround. Right now I just strip the last char of the Button's x:Name...
Port_selected is an event handler and you cannot pass a string value to this one.
If you are familiar with the MVVM design pattern there are better ways to solve this, for example using commands and command parameters, but the easiest solution given your current setup is probably to get a reference to the TextBlock in the event handler and then get the value of its Name property, e.g.:
private void Port_selected(object sender, RoutedEventArgs e)
{
Button btn = (Button)sender;
StackPanel stackPanel = btn.Parent as StackPanel;
if (stackPanel != null)
{
TextBlock textBlock = stackPanel.Children
.OfType<TextBlock>()
.FirstOrDefault(x => !string.IsNullOrEmpty(x.Name));
if (textBlock != null)
{
string name = textBlock.Name;
//...
}
}
}
I would like to modify the selection behavior of the DataGrid in the following way. Normally when you have multiple rows selected, and then you click one of the items already selected, the selection is reset to only the clicked item. I would like to change it such that if one of the multi-selected rows is clicked without any keyboard modifiers, the selection is not modified. The goal of this is to allow a multi-item drag-drop.
I noticed that when aforementioned default behavior is activated, the call stack includes:
at System.Windows.Controls.DataGrid.OnSelectionChanged(SelectionChangedEventArgs e)
at System.Windows.Controls.Primitives.Selector.SelectionChanger.End()
at System.Windows.Controls.DataGrid.MakeFullRowSelection(ItemInfo info, Boolean allowsExtendSelect, Boolean allowsMinimalSelect)
at System.Windows.Controls.DataGrid.HandleSelectionForCellInput(DataGridCell cell, Boolean startDragging, Boolean allowsExtendSelect, Boolean allowsMinimalSelect)
at System.Windows.Controls.DataGridCell.OnAnyMouseLeftButtonDown(MouseButtonEventArgs e)
at System.Windows.RoutedEventArgs.InvokeHandler(Delegate handler, Object target)
at System.Windows.RoutedEventHandlerInfo.InvokeHandler(Object target, RoutedEventArgs routedEventArgs)
at System.Windows.EventRoute.InvokeHandlersImpl(Object source, RoutedEventArgs args, Boolean reRaised)
at System.Windows.UIElement.ReRaiseEventAs(DependencyObject sender, RoutedEventArgs args, RoutedEvent newEvent)
at System.Windows.UIElement.OnMouseDownThunk(Object sender, MouseButtonEventArgs e)
at System.Windows.RoutedEventArgs.InvokeHandler(Delegate handler, Object target)
Therefore it looks like I should be able to modify the behavior by overriding DataGridCell.OnMouseLeftButtonDown, something like this:
class MultiDragDataGridCell : DataGridCell
{
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
// This allows users to click-and-drag a multi-selection by handling the event before
// the default behavior (deselecting everything but the clicked cell) kicks in.
if (IsSelected && Keyboard.Modifiers == ModifierKeys.None)
{
e.Handled = true;
}
base.OnMouseLeftButtonDown(e);
}
}
However, I'm having trouble getting the DataGrid to create a MultiDragDataGridCell instead of a normal DataGridCell, since the class that instantiates DataGridCell is internal. Anyone know how I can achieve that, or if there's another way of achieving the behavior I want?
Other things I tried:
Styling the DataGridCell to add a handler to MouseLeftButtonDown. This doesn't work because it executes after the selection has already changed.
Styling the DataGridCell to add a handler to PreviewMouseLeftButtonDown. This works, but it prevents me from clicking any buttons, etc. inside the cell.
Note: This answer only tries to provide a solution to following issue mentioned in the question; not how to override the grid's selection behavior. I am hoping that once you have a custom DataGridCell in place, it can be a good starting point for what you are trying to do.
However, I'm having trouble getting the DataGrid to create a MultiDragDataGridCell instead of a normal DataGridCell, since the class that instantiates DataGridCell is internal. Anyone know how I can achieve that..
Solution: In order to ensure that the DataGrid uses your custom DataGridCell - you need to re-template your DataGridRow to use an extended version of DataGridCellsPresenter which in-turn will provide your custom DataGridCell.
Please refer following sample code:
Extending DataGrid controls
public class ExtendedDataGrid : DataGrid
{
protected override DependencyObject GetContainerForItemOverride()
{
//This provides the DataGrid with a customized version for DataGridRow
return new ExtendedDataGridRow();
}
}
public class ExtendedDataGridRow : DataGridRow { }
public class ExtendedDataGridCellsPresenter : System.Windows.Controls.Primitives.DataGridCellsPresenter
{
protected override DependencyObject GetContainerForItemOverride()
{
//This provides the DataGrid with a customized version for DataGridCell
return new ExtendedDataGridCell();
}
}
public class ExtendedDataGridCell : DataGridCell
{
// Your custom/overridden implementation can be added here
}
Re-template DataGridRow in XAML (a more comprehensive template can be found at this link - I am only using a watered-down version of it for sake of readability).
<Style TargetType="{x:Type local:ExtendedDataGridRow}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ExtendedDataGridRow}">
<Border x:Name="DGR_Border"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
SnapsToDevicePixels="True">
<SelectiveScrollingGrid>
<SelectiveScrollingGrid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</SelectiveScrollingGrid.ColumnDefinitions>
<SelectiveScrollingGrid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</SelectiveScrollingGrid.RowDefinitions>
<!-- Make sure to register your custom DataGridCellsPresenter here as following -->
<local:ExtendedDataGridCellsPresenter Grid.Column="1"
ItemsPanel="{TemplateBinding ItemsPanel}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
<DataGridDetailsPresenter Grid.Column="1"
Grid.Row="1"
Visibility="{TemplateBinding DetailsVisibility}"
SelectiveScrollingGrid.SelectiveScrollingOrientation=
"{Binding AreRowDetailsFrozen,
ConverterParameter={x:Static SelectiveScrollingOrientation.Vertical},
Converter={x:Static DataGrid.RowDetailsScrollingConverter},
RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"/>
<DataGridRowHeader Grid.RowSpan="2"
SelectiveScrollingGrid.SelectiveScrollingOrientation="Vertical"
Visibility="{Binding HeadersVisibility,
ConverterParameter={x:Static DataGridHeadersVisibility.Row},
Converter={x:Static DataGrid.HeadersVisibilityConverter},
RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" />
</SelectiveScrollingGrid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
And, your extended DataGrid's visual tree have the custom datagrid-cells:
Also, please note, it is not mandatory to extend DataGrid, or DataGridRow to provide a custom DataGridCell - you can achieve the same result by just extending DataGridCellsPresenter (and, updating DataGridRow's control-template to use the extended version)
Actually you had a solution: make a styling for DataGridCell and set an event handler, but I suppose there was a logical error in your event handler: you have set e.Handled to true, if DataGridCell was selected, so the inner controls could not be manipulated, because the default behavior for DataGrid is first select/unselect the row/cell(and only then manipulate the inner controls), so in case you have multiple selection the clicked upon row/cell was selected, so actually you only have needed to prevent selection of row/cell clicked upon in case of multiple selection.
I suppose this should work as you have expected:
<DataGrid.Resources>
<Style TargetType="DataGridCell">
<EventSetter Event="PreviewMouseLeftButtonDown" Handler="PreviewMouseDown"/>
</Style>
</DataGrid.Resources>
private void PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
var cell = sender as DataGridCell; if (cell == null) { return; }
DataGrid parGrid = null;
var visParent = VisualTreeHelper.GetParent(cell);
while (parGrid==null && visParent != null)
{
parGrid = visParent as DataGrid;
visParent = VisualTreeHelper.GetParent(visParent);
}
if (parGrid==null) { return; }
e.Handled = cell.IsSelected && Keyboard.Modifiers == ModifierKeys.None && parGrid.SelectedItems.Count > 1;
}
The only thing I could come up with feels like a big hack, so better don't use it as is. But it might be a starting point for finding your own solution.
Basic ideas:
Execute some event handlers even on handled events with EventManager.RegisterClassHandler. This needs some refinement or you end up messing with all cells in the whole application
Register for cell selection restore when left mouse click on selected cell without modifiers
Consider drag & drop only after left mouse click on selected cell (otherwise the user experience becomes REALLY strange for this combination of requirements)
Restore selected cells if previously registered and cells are unselected
Remove cell selection restore registration after restoring or when mouse does other things (mouse up or mouse move)
Customized data grid code:
public class MyDataGrid : DataGrid
{
static MyDataGrid()
{
EventManager.RegisterClassHandler(typeof(DataGridCell), UIElement.PreviewMouseLeftButtonDownEvent, new MouseButtonEventHandler(PreviewMouseLeftButtonDownHandler));
EventManager.RegisterClassHandler(typeof(DataGridCell), UIElement.MouseLeftButtonUpEvent, new MouseButtonEventHandler(MouseLeftButtonUpHandler), true);
EventManager.RegisterClassHandler(typeof(DataGridCell), UIElement.MouseMoveEvent, new MouseEventHandler(MouseMoveHandler), true);
}
private static bool restoreNextCells = false;
private static bool isSelectedCell = false;
private static void PreviewMouseLeftButtonDownHandler(object sender, MouseButtonEventArgs e)
{
var cell = sender as DataGridCell;
isSelectedCell = cell.IsSelected;
restoreNextCells = cell.IsSelected && Keyboard.Modifiers == ModifierKeys.None;
}
private static void MouseMoveHandler(object sender, MouseEventArgs e)
{
var cell = sender as DataGridCell;
if (isSelectedCell && e.LeftButton == MouseButtonState.Pressed && cell.IsSelected && Keyboard.Modifiers == ModifierKeys.None)
{
DragDrop.DoDragDrop(cell, new ObjectDataProvider(), DragDropEffects.All);
}
restoreNextCells = false;
isSelectedCell = false;
}
private static void MouseLeftButtonUpHandler(object sender, MouseButtonEventArgs e)
{
restoreNextCells = false;
isSelectedCell = false;
}
protected override void OnSelectedCellsChanged(SelectedCellsChangedEventArgs e)
{
if (restoreNextCells && e.RemovedCells.Count > 0)
{
foreach (DataGridCellInfo item in e.RemovedCells)
{
SelectedCells.Add(item);
}
restoreNextCells = false;
}
base.OnSelectedCellsChanged(e);
}
}
Use with multi cell selection.
<local:MyDataGrid SelectionMode="Extended" SelectionUnit="Cell">
Hope I didn't leave out any important part in my explanation... ask if anything is unclear.
I've got a ListBox that is displaying a dynamic number of TextBoxes. The user will enter text into these boxes. When the Submit button is clicked, I need to be able to access the text the user has input, should be at ListBox.Items, like so:
//Called on Submit button click
private void SaveAndSubmit(object sender, ExecutedRoutedEventArgs e)
{
var bounds = MyListBox.Items;
}
But MyListBox.Items doesn't change after I initially set the ItemsSource, here:
//Field declaration
//Bounds is containing a group of strings that represent the boundaries
//for a contour plot. The min/max values are stored at the front and back
//of the group. However, there can be any number of dividers in between.
public ObservableCollection<string> Bounds { get; set; }
...
//Initialize Bounds in the constructor
//Called when the selected item for DVList (an unrelated ListBox) is changed
private void DVSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var selectedDV = DVList.SelectedItem as DVWrapper;
if (selectedDV != null)
{
//Setting min/max
Bounds[0] = selectedDV.MinValue;
Bounds[Bounds.Count - 1] = selectedDV.MaxValue;
MyListBox.ItemsSource = Bounds;
}
}
My XAML looks like this:
<Window.Resources>
<Style x:Key="BoundsStyle" TargetType="{x:Type ListBoxItem}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Grid>
...
<TextBox/>
</Grid>
</DataTemplate>
</Setter.Value>
</Setter>
<Setter Property="Focusable" Value="False"/>
</Style>
</Window.Resources>
...
<ListBox Name="MyListBox"
ItemContainerStyle="{StaticResource BoundsStyle}"/>
So when SaveAndSubmit is called, bounds ends up being what I had originally set it to in DVSelectionChanged. In other words, the listbox is not updating based on what the user has input into the textboxes contained in listbox. How can I get the updated ListBoxItems? I think my problem is similar to this one, but it's not working for me at the moment.
When I step through in the debugger, I can get individual ListBoxItems. However, their Content is empty. I'm looking into that right now.
You need to bind content of the textbox.
<TextBox/> need to change to <TextBox Content="{Binding}"/>
But follow MVVM else it will be difficult to find these errors.
I have a popup with StaysOpen=False so I want to close it by clicking anywhere outside of popup. Inside a popup I have a DataGrid. If I open popup and then click somewhere else the popup will be closed. But it won't happen if before clicking outside of popup I will click on column header in DataGrid. Test XAML:
<Window x:Class="Test.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525" VerticalAlignment="Center" HorizontalAlignment="Center" Foreground="Black">
<Grid>
<ToggleButton x:Name="btn" VerticalAlignment="Top">Open</ToggleButton>
<Popup StaysOpen="False" IsOpen="{Binding IsChecked, ElementName=btn}" >
<DataGrid Width="150" Height="150">
<DataGrid.Columns>
<DataGridTextColumn Header="Column" />
</DataGrid.Columns>
</DataGrid>
</Popup>
</Grid>
</Window>
I think that it happens because column header captures the mouse on click and popup doesn't receive mouse events anymore. I've tried to add a handler on LostMouseCapture event in order to capture mouse back by popup but it doesn't seem to work that easy. Any ideas?
Maybe it will help.
Attached behavior:
public class DataGridColumnHeaderReleaseMouseCaptureBehavior {
public static DataGrid GetReleaseDGCHeaderBehavior(DependencyObject obj) {
return (DataGrid)obj.GetValue(ReleaseDGCHeaderBehaviorProperty);
}
public static void SetReleaseDGCHeaderBehavior(DependencyObject obj, Boolean value) {
obj.SetValue(ReleaseDGCHeaderBehaviorProperty, value);
}
public static readonly DependencyProperty ReleaseDGCHeaderBehaviorProperty =
DependencyProperty.RegisterAttached("ReleaseDGCHeaderBehavior",
typeof(DataGrid),
typeof(DataGridColumnHeaderReleaseMouseCaptureBehavior),
new UIPropertyMetadata(default(DataGrid), OnReleaseDGCHeaderBehaviorPropertyChanged));
private static Popup _popup;
private static void OnReleaseDGCHeaderBehaviorPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var oldGrid = (DataGrid)e.OldValue;
if (oldGrid != null)
oldGrid.MouseLeave -= OnMouseLeave;
var refSender = d as Popup;
_popup = refSender;
if (refSender != null) {
var refGrid = e.NewValue as DataGrid;
if (refGrid != null) {
refGrid.MouseLeave += OnMouseLeave;
}
}
}
static void OnMouseLeave(object sender, MouseEventArgs args) {
if (_popup != null)
typeof(Popup).GetMethod("EstablishPopupCapture", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).Invoke(_popup, null);
}
}
XAML:
<Popup x:Name="popup"
bhvrs:DataGridColumnHeaderReleaseMouseCaptureBehavior.ReleaseDGCHeaderBehavior="{Binding ElementName=dataGrid}">
<DataGrid x:Name="dataGrid"/>
</Popup>
I think you've stumbled onto just a plain old bug. I've reproduced this and could not find a reasonable way to get it working. I think you should file a bug with Microsoft. It seems like a component that captures the mouse and the uncaptures it doesn't restore the capture to the originally capturing component.
I had a similar problem recently altough not exactly the same, and it was in Silverlight. I hacked my way through it by searching the required control (in your case the popup I guess) with the GetTemplatedParent function, in the required event handler of the 'misbehaving' control, and programatically do what I wanted to do on it.
This is not a nice solution, and doesn't solve all the problems, but you can give it a try. Be sure you comment what have you done, because it can turn into a mess.
i had the same problem,and did something like this:
private void YourDataGrid_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
YourDataGrid.CaptureMouse();
YourDataGrid.ReleaseMouseCapture();
}
but i'm looking for something better yet...
Is there a way to make the selectionchanged event fire every time a selection in the listview is clicked, instead of only when it changes?
For example, lets say i have a listview with only one object in it. The user clicks that object, and that object contains information that populates some textboxes below. The user starts changing some of the values in these textboxes (which are not bound to the object). They then decide that they dont want what is in those text boxes so they'd like to reset everything to what is in the object in the listview. But when they click the one object in the listview, nothing happens, because the selection has not changed.
Hope that makes sense. Anyone know how I can get around this?
The ListView.SelectionChanged and ListViewItem.Selected events are not going to re-fire if the item is already selected. If you need to re-fire it, you could 'deselect' the item when the event fires.
private void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
foreach (var item in e.AddedItems.OfType<ListViewItem>())
{
Trace.WriteLine("ListViewItem Selected");
item.IsSelected = false;
}
}
Thus allowing you to re-select it ad nauseum. However, if you don't need the actual selection then you should be using an ItemsControl.
If you do want to maintain the select-ability of the item(s) then you should look at registering to a different event than ListView.SelectionChanged, or ListView.Selected. One that works well for this is PreviewMouseDown, as like the initial item selection we want it to occur on both left and right clicks. We could attach it to the single ListViewItem, but since the list may at some point gain more items, we can assign it to all items by using the ItemContainerStyle property of the ListView.
<ListView SelectionChanged="ListView_SelectionChanged">
<ListView.ItemContainerStyle>
<Style TargetType="{x:Type ListViewItem}">
<EventSetter Event="PreviewMouseDown"
Handler="ListViewItem_PreviewMouseDown" />
</Style>
</ListView.ItemContainerStyle>
<ListViewItem>Item 1</ListViewItem>
<ListViewItem>Item 2</ListViewItem>
<ListViewItem>Item 3</ListViewItem>
<ListViewItem>Item 4</ListViewItem>
</ListView>
private void ListViewItem_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
Trace.WriteLine("ListViewItem Clicked: " + (sender as ListViewItem).Content);
}
private void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if(ListView.SelectedIndex != -1)
{
//to do staff
}
ListView.SelectedIndex = -1;
}
also we can use this one!
<ListView x:Name="ListView"
Height="Auto" SelectionChanged="ListView_OnSelectionChanged"
Width="260"
Margin="0,-12,0,-25">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding name_to_show_menu,Mode=TwoWay}" Tapped="UIElement_OnTapped"></TextBlock>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
and in code behind
private void UIElement_OnTapped(object sender, TappedRoutedEventArgs e)
{
//this fire every time
}