WPF - context menu over DataGrid - c#

My DataGrid is bound to a data source, which is a database. When user right clicks somewhere over the DataGrid control, I'd like to be able to recognize over which column he or she did it. Scenario is as follows - if ContextMenu is opened over a column holding dates, then (for example) I would like to present him with options to filter out dates smaller, greater or equal to the date selected.
<DataGrid.Resources>
<Helpers:BindingProxy x:Key="proxy" Data="{Binding}" />
</DataGrid.Resources>
<DataGrid.ContextMenu>
<ContextMenu DataContext="{Binding Path=DataContext}">
<MenuItem Header="Cokolwiek" Command="{Binding Source={StaticResource proxy}, Path=Data.FK}" CommandParameter="{Binding RelativeSource={RelativeSource Self}, Path=Parent.PlacementTarget.DataContext}"/>
</ContextMenu>
</DataGrid.ContextMenu>
PlacementTarget is a reference to the DataGrid, and I'd like it to be a reference to DataGridColumn.
BindingProxy class:
public class BindingProxy : Freezable {
protected override Freezable CreateInstanceCore() {
return new BindingProxy();
}
public object Data {
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
// Using a DependencyProperty as the backing store for Data.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object),
typeof(BindingProxy), new UIPropertyMetadata(null));
}

What you can do is hook up to the PreviewMouseUp event so you can look at the Source property of the Event that is raised.
With the exception of direct events, WPF defines most routed events in pairs - one tunnelling and the other bubbling. The tunnelling event name always begins with 'Preview' and is raised first. This gives parents the chance to see the event before it reaches the child. This is followed by the bubbling counterpart. In most cases, you will handle only the bubbling one. The Preview would be usually used to
block the event (e.Handled = true) cause the parent to do something in
advance to normal event handling.
e.g. if UI Tree = Button contains Grid contains Canvas contains Ellipse
Clicking on the ellipse would result in (MouseDownButton is eaten up by Button and Click is raised instead.)
private void OnPreviewMouseUp(object sender, MouseButtonEventArgs mouseButtonEventArgs)
{
var source = mouseButtonEventArgs.Source;
// Assuming the DataGridColumn's Template is just a TextBlock
// but depending on the Template which for sure should at least inherit from FrameworkElement to have the Parent property.
var textBlock = source as TextBlock;
// Not a good check to know if it is a holding dates but it should give you the idea on what to do
if (textBlock != null)
{
var dataGridColumn = textBlock.Parent as DataGridColumn;
if (dataGridColumn != null)
{
if ((string) dataGridColumn.Header == "Holding Dates")
{
// Show context menu for holding dates
}
}
}
// Other stuff
else if (somethingElse)
{
// Show context menu for other stuff
}
}

Related

UserControl with custom ItemsSource doesn't detect changes

I have simple UserControl where is defined property ItemsSource
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(Dictionary<string, object>), typeof(UserControl1), new FrameworkPropertyMetadata(null,
new PropertyChangedCallback(UserControl1.OnItemsSourceChanged)));
public Dictionary<string, object> ItemsSource
{
get { return (Dictionary<string, object>)GetValue(ItemsSourceProperty); }
set
{
SetValue(ItemsSourceProperty, value);
}
}
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
UserControl1 control = (UserControl1)d;
control.DisplayInControl();
}
I want to make this property update dynamically, but I am wondered why OnItemsSourceChanged doesn't fired every time when something happend with ItemsSource. So I am upset.
I've tried Custom ItemsSource property for a UserControl but this doesn't help or I've written bad newValueINotifyCollectionChanged_CollectionChanged function
My control is from this post CodeProject
My Code :
UserControl XAML - http://pastie.org/10606317
UserControl CodeBehind - http://pastie.org/10606322
Control Usage -
<controls:MultiSelectComboBox SelectedItems="{Binding SelectedCategories, Mode=TwoWay}" Grid.Column="0" Grid.Row="0" x:Name="CategoriesFilter" DefaultText="Category" ItemsSource="{Binding Categories }" Style="{StaticResource FiltersDropDowns}"/>
Update : I've made small step to solution. I have next style :
<Style.Triggers>
<DataTrigger Binding="{Binding ItemsSource, RelativeSource={RelativeSource Self}}" Value="{x:Null}">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
<DataTrigger Binding="{Binding ItemsSource.Count, RelativeSource={RelativeSource Self}}" Value="0">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>
which I apply to my control (make control disabled if no itemSource). As I update control source on click, I see that control becomes enabled, so ItemsSource aren't empty (from start it is). So problem now is just in Redrawing control content if I understand this behaviour correctly.
If you have a dictionary as your data type, and you add or remove a value from the dictionary, then your property did not actually change. This event will only fire if you have actually set this property to reference a different dictionary.
If you need wpf to automatically detect if an item is added or removed from the dictionary, you should use an ObservableCollection.
Replace Dictionary with ObservableCollection, Dictionary won't fire the propertyChanged event when add, update, delete an item.
Write your own Dictionary to fire the propertychanged event manually.
I see that the problem is that you are building a new ItemsSource for the ComboBox control within your custom UserControl, and that you are expecting that MultiSelectCombo.ItemsSource to stay synced with your UserControl1.ItemsSource. This can't happen when simply binding a Dictionary, and it won't happen even when binding an ObservableCollection -- unless you explicitly handle it.
To accomplish what you are after, first you will need ItemsSource of your custom control to be of a type that does notify us of collection changes, such as the ObservableCollection (which I'll use for this example, as you've done in your links above). Then, you'll need to update the ItemsSource of your ComboBox within your control, to keep them in sync.
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (MultiSelectComboBox)d;
// Since we have to listen for changes to keep items synced, first check if there is an active listener to clean up.
if (e.OldValue != null)
{
((ObservableCollection<Node>)e.OldValue).CollectionChanged -= OnItemsSource_CollectionChanged;
}
// Now initialize with our new source.
if (e.NewValue != null)
{
((ObservableCollection<Node>)e.NewValue).CollectionChanged += OnItemsSource_CollectionChanged;
control.DisplayInControl();
}
}
private void OnItemsSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
DisplayInControl();
}
Now, that said, the solution above is for a more generic case, where you might need to do something with the ItemsSource given to your custom control before passing it onto your MultiSelectCombo.ItemsSource. In your current case, however, you are simply building a collection of Node to exactly match the given collection of Node. If that's guaranteed to be the case, your solution can be much, much simpler:
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (MultiSelectComboBox)d;
control.MultiSelectCombo.ItemsSource = (IEnumerable)e.NewValue;
}
Just let the wrapped ComboBox use the ItemsSource given to your custom control.

Determine the selected HubSection in a Hub with two HubSections

I have a hub control with two HubSections. When selected HubSection changes, I want to change the contents of the AppBar with section specific buttons.
Listening SectionsInViewChanged event is the general solution recommended to implement this behavior but this event is not fired when there are only two HubSections.
Is there another event that can be used to determine the current HubSection?
Thanks.
#Depechie has pointed you in the right direction.. You can use the SelectionHub control I created and add an event to it that fires when the selected index changes
public event EventHandler SelectionChanged;
private static void OnSelectedIndexChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var hub = d as SelectionHub;
if (hub == null) return;
// do not try to set the section when the user is swiping
if (hub._settingIndex) return;
// No sections?!?
if (hub.Sections.Count == 0) return;
hub.OnSelectionChanged();
}
private void OnSelectionChanged()
{
var section = Sections[SelectedIndex];
ScrollToSection(section);
var handler = SelectionChanged;
if(handler != null)
{
handler(this, EventArgs.Empty);
}
}
You can extend this by adding selection changed event args to it. With this you can then subscribe to the event in your code behind and change the app bar buttons based on the index.
After reading Shawn's article suggested by #Depechie. I tried to implement the same solution in my app in order to update contents of the AppBar with section specific buttons.
Despite my efforts I was unable to make it work so I modified some parts of the solution. I used the behavior solution and changed only the ScrollerOnViewChangedfunction as follows. This might not be the best way or may cause unexpected results in different scenarios but in my case it worked without a problem.
private void ScrollerOnViewChanged(object sender, ScrollViewerViewChangedEventArgs scrollViewerViewChangedEventArgs)
{
_settingIndex = true;
ScrollViewer scrollViewer = sender as ScrollViewer;
if (scrollViewer.HorizontalOffset > (scrollViewer.ViewportWidth / 2))
SelectedIndex = 1;
else
SelectedIndex = 0;
_settingIndex = false;
}
After that I added a property to my viewmodel in order to store selected index.
private int _selectedIndex;
public int SelectedIndex
{
get { return _selectedIndex; }
set
{
SetProperty(ref this._selectedIndex, value);
}
}
I used the behavior in the XAML in order to update SelectedIndex in my ViewModel.
<Hub>
<i:Interaction.Behaviors>
<behaviors:HubSelectionBehavior SelectedIndex="{Binding SelectedIndex, Mode=TwoWay}" />
</i:Interaction.Behaviors>
<HubSection>...</HubSection>
<HubSection>...</HubSection>
</Hub>
The last thing to do was to set the visibility of AppBarButtons using this property. SectionIndexToVisibilityConverter compares SelectedIndex to ConverterParameter and returns Visibility.Visible if they are equal.
<CommandBar>
<AppBarButton Label="Open" Icon="World" Command="{Binding OpenInBrowserCommand}" Visibility="{Binding SelectedIndex, Converter={StaticResource SectionIndexToVisibilityConverter}, ConverterParameter=0}"/>
<AppBarButton Label="Previous" Icon="Back" Command="{Binding PreviousAnswerCommand}" Visibility="{Binding SelectedIndex, Converter={StaticResource SectionIndexToVisibilityConverter}, ConverterParameter=1}"/>
<AppBarButton Label="Next" Icon="Forward" Command="{Binding NextAnswerCommand}" Visibility="{Binding SelectedIndex, Converter={StaticResource SectionIndexToVisibilityConverter}, ConverterParameter=1}"/>
</CommandBar>
Thanks #Depechie for suggesting the article and #Shawn for writing the article : )

How do I get a Caliburn Micro Action when a Datagrid's scrollbar is scrolled to the end?

I have a WPF Datagrid that is displaying a structured log from a back-end system. The logs can be gigantic, so I only fetch a few hundred entries at time. I want to trigger retrieving more entries when the scrollbar thumb hits the 'bottom' of the scroll area.
I found this code-behind solution for sensing the end of the scroll, but I'm using Caliburn Micro. So I tried hooking up the ScrollChanged event, so I could process it in the view model (not my favorite solution, but I appear to be running out of options). The "obvious" implicit caliburn binding of
cal:Message.Attach="[Event ScrollViewer.ScrollChanged] = [Action DoScrollAction($eventArgs)]"
doesn't work, and neither did the explicit
<i:Interaction.Triggers>
<i:EventTrigger EventName="ScrollViewer.ScrollChanged">
<cal:ActionMessage MethodName="DoScrollAction">
<cal:Parameter Value="$eventargs" />
</cal:ActionMessage>
</i:EventTrigger>
</i:Interaction.Triggers>
approach. Is there something baked into Caliburn Micro that addresses these attached events? Is there a better event to use for sensing the end of the scroll area in a Datagrid that doesn't have me processing ScrollChanged events in a view model?
So I discovered Joymon's solution for addressing the attached events after posting my question, and used his RoutedEventTrigger class, combined with the code sensing the end-of-scroll condition in my view model.
For the record, here are the pieces of the solution:
xmlns:wpfCommon="clr-namespace:WPFCommon;assembly=WPFCommon"
...
<DataGrid x:Name="SCPLog">
<i:Interaction.Triggers>
<wpfCommon:RoutedEventTrigger RoutedEvent="ScrollViewer.ScrollChanged">
<cal:ActionMessage MethodName="DoScroll">
<cal:Parameter Value="$eventargs" />
</cal:ActionMessage>
</wpfCommon:RoutedEventTrigger>
</i:Interaction.Triggers>
</DataGrid>
And a variation on Joymon's RoutedEventTrigger, which I placed in my own WPFCommon library:
public class RoutedEventTrigger : EventTriggerBase<DependencyObject>
{
public RoutedEvent RoutedEvent { get; set; }
protected override void OnAttached()
{
var behavior = base.AssociatedObject as Behavior;
var associatedElement = base.AssociatedObject as FrameworkElement;
if (behavior != null)
associatedElement = ((IAttachedObject)behavior).AssociatedObject as FrameworkElement;
if (associatedElement == null)
throw new ArgumentException("Routed Event trigger can only be associated to framework elements");
if (RoutedEvent != null)
associatedElement.AddHandler(RoutedEvent, new RoutedEventHandler(this.OnRoutedEvent));
}
void OnRoutedEvent(object sender, RoutedEventArgs args) { base.OnEvent(args); }
protected override string GetEventName() { return RoutedEvent.Name; }
}
With the end-of-scroll detection in the view model:
public void DoScroll(ScrollChangedEventArgs e)
{
var scrollViewer = e.OriginalSource as ScrollViewer;
if (scrollViewer != null && // Do we have a scroll bar?
scrollViewer.ScrollableHeight > 0 && // Avoid firing the event on an empty list.
scrollViewer.VerticalOffset == scrollViewer.ScrollableHeight && // Are we at the end of the scrollbar?
{
// Do your end-of-scroll code here...
}
}
If someone knows a better way to handle the end-of-scroll event, e.g. doing it in XAML, I'd love to hear it.

in Silverlight DataGrid, CheckBox's DataContext sometimes null when Checked/Unchecked

I have a DataGrid. One of the columns is a template with a CheckBox in it. When the Checked or Unchecked events trigger, (it happens with both) the CheckBox's DataContext is sometimes null, which causes my code to error. It seems to be null most often if the mouse is moving while you press and release the button quickly (it's intermittent).
I listened for changes to the DataContext of the CheckBox by making views:ListenCheckBox (extends CheckBox) and attaching a binding, and it's never set to null, but it is set from null to a Task at times I wouldn't expect, i.e. after the DataGrid has been totally generated and you're checking/unchecking boxes. Immediately after the [un]checked event runs with a null DataContext, I get the notification that shows the DataContext changed from null to a Task, so it appears that when I get a null DataContext, it's because it hadn't actually set the DataContext by the time it ran the Checked/Unchecked event.
Also, I added Tag="{Binding}" to the CheckBox for debugging. The Tag is not null (i.e. it has the proper object) more often than the DataContext, but still not all the time.
Here are the relevant bits of the XAML code:
<navigation:Page.Resources>
<sdk:DataGridTemplateColumn x:Key="DeleteOrPrintSelect" Header="Delete Or Print Notes Selection">
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<views:ListenCheckBox IsChecked="{Binding DeleteOrPrintNotesSelection, Mode=TwoWay}" Checked="DeletePrintNotesCheckBox_Changed" Unchecked="DeletePrintNotesCheckBox_Changed" HorizontalAlignment="Center" VerticalAlignment="Center" Tag="{Binding}" />
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>
</sdk:DataGridTemplateColumn>
</navigation:Page.Resources>
<sdk:DataGrid x:Name="dataGrid1" Grid.Column="1" Grid.Row="2" AutoGeneratingColumn="dataGrid1_AutoGeneratingColumn">
<sdk:DataGrid.RowGroupHeaderStyles>
[removed]
</sdk:DataGrid.RowGroupHeaderStyles>
</sdk:DataGrid>
And the relevant code behind:
// Create a collection to store task data.
ObservableCollection<Task> taskList = new ObservableCollection<Task>();
[code adding Tasks to taskList removed]
PagedCollectionView panelListView = new PagedCollectionView(taskList);
this.dataGrid1.ItemsSource = panelListView;
}
private void dataGrid1_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
if (e.PropertyName == "DeleteOrPrintNotesSelection")
{
e.Column = Resources["DeleteOrPrintSelect"] as DataGridTemplateColumn;
}
else
{
e.Column.IsReadOnly = true;
}
}
private void DeletePrintNotesCheckBox_Changed(object sender, RoutedEventArgs e)
{
try
{
var cb = sender as CheckBox;
var t = cb.DataContext as Task;
t.DeleteOrPrintNotesSelection = cb.IsChecked == true;
PagedCollectionView pcv = this.dataGrid1.ItemsSource as PagedCollectionView;
ObservableCollection<Task> taskList = pcv.SourceCollection as ObservableCollection<Task>;
bool anySelected = taskList.Any(x => x.DeleteOrPrintNotesSelection);
this.btnPrint.IsEnabled = anySelected;
this.btnDelete.IsEnabled = anySelected;
}
catch (Exception ex)
{
ErrorMessageBox.Show("recheck", ex, this);
}
}
Any ideas? Thanks in advance.
I found that the problem happened when you double click on the cell and it moved it to the cell editing template. In my case, I didn't have a cell editing template defined, so it used the same cell template, but instead of not changing anything, it apparently decided to make a new check box. I set the column's IsReadOnly property to true, and it fixed it. An alternate solution:
DataContext="{Binding}" (in XAML, or the code equivalent:)
cb.SetBinding(FrameworkElement.DataContextProperty, new Binding());
I'm not sure why this one fixes it, since I thought the default DataContext is {Binding}. Perhaps it's a Silverlight bug, and it gets set in a different order if you define it explicitly instead of leaving it the default.

Problem with DataGrid Rows in WPF

I have a datagrid where I have two buttons on each row - one for moving a row up and one for moving a row down.
Each button has a command for allowing the user to move the selected row in either direction. The problem that I am facing is that it not working. I think the problem that I may have is that the other controls (combo boxes) on the rows are bound to data sources through the MVVM model where I am manipulating the rows on the code behind of the XAML thinking this would be the logical place in which to do it.
The code I have for one of the buttons is below:
private void MoveRowDown(object sender, ExecutedRoutedEventArgs e)
{
int currentRowIndex = dg1.ItemContainerGenerator.IndexFromContainer(dg1.ItemContainerGenerator.ContainerFromItem(dg1.SelectedItem));
if (currentRowIndex >= 0)
{
this.GetRow(currentRowIndex + 1).IsSelected = true;
}
}
private DataGridRow GetRow(int index)
{
DataGridRow row = (DataGridRow)dg1.ItemContainerGenerator.ContainerFromIndex(index);
if (row == null)
{
dg1.UpdateLayout();
dg1.ScrollIntoView(selectedAttributes.Items[index]);
row = (DataGridRow)dg1.ItemContainerGenerator.ContainerFromIndex(index);
}
return row;
}
You have to manipulate the CollectionView for the DataGrid. The CollectionView is responsible for how your data looks like basically. Here is a small example:
Let's assume you've bound your DataGrid to an ObservableCollection<T> named Items, and that T has a property called Index on which is sorted.
Initialize the ICollectionView in your ViewModel like this:
private ICollectionView cvs;
ctor(){
/*your default init stuff*/
/*....*/
cvs = CollectionViewSource.GetDefaultView(items);
view.SortDescriptions.Add(new SortDescription("Index",ListSortDirection.Ascending));
}
Now you can bind your button command to the Up command (also in your ViewModel):
private ICommand upCommand;
public ICommand Up
{
get { return upCommand ?? (upCommand = new RelayCommand(p => MoveUp())); }
}
private void MoveUp()
{
var current = (Item)view.CurrentItem;
var i = current.Index;
var prev = Items.Single(t => t.Index == (i - 1));
current.Index = i - 1;
prev.Index = i;
view.Refresh(); //you need to refresh the CollectionView to show the changes.
}
WARNING: you have to add checks to see if there is a previous item etc. Alternatively you can specify a CanExecute delegate which checks the Index of the item and enables/disables the button.
(RelayCommand courtesy of Josh Smith/Karl Shifflett, can't find the correct link anymore)
Bind the command to you button like this (assuming your viewmodel is the DataContext of your window):
<DataGridTemplateColumn >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="Up"
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}},
Path=DataContext.Up}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
Hope this helps!

Categories

Resources