DataGrid, Implementing a custom command column
-
lunedì 30 aprile 2012 17:45
Hi,
I am trying to implement a custom DataGrid column inherited from DataGridTextColumn. It extends the DataGridTextColumn by adding a ContextMenu to the cell containing one menu item to select the cell value. It exposes two dependency properties: A Command property and a CommandParameter property. These can be used to bind a ICommand object to the column.
public class DataGridCommandColumn : DataGridTextColumn { public ICommand Command { get { return (ICommand)GetValue(CommandProperty); } set { SetValue(CommandProperty, value); } } public static readonly DependencyProperty CommandProperty = DependencyProperty.Register("Command", typeof(ICommand), typeof(DataGridCommandColumn), new UIPropertyMetadata(null)); public object CommandParameter { get { return GetValue(CommandParameterProperty); } set { SetValue(CommandParameterProperty, value); } } public static readonly DependencyProperty CommandParameterProperty = DependencyProperty.Register("CommandParameter", typeof(object), typeof(DataGridCommandColumn), new UIPropertyMetadata(null)); protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem) { TextBlock textBlock = (TextBlock)base.GenerateElement(cell, dataItem); MenuItem menuItem = new MenuItem { HeaderStringFormat = "Navigate to {0}" }; ApplyBinding(menuItem, MenuItem.HeaderProperty); menuItem.Click += new RoutedEventHandler(menuItem_Click); textBlock.ContextMenu = new ContextMenu(); textBlock.ContextMenu.Items.Add(menuItem); return textBlock; } private void menuItem_Click(object sender, RoutedEventArgs e) { this.Command.Execute(null); } // Copied from DataGridTextColumn because it's not protected there either. Seems like it should be. internal void ApplyBinding(DependencyObject target, DependencyProperty property) { var binding = Binding; if (binding == null) { BindingOperations.ClearBinding(target, property); } else { BindingOperations.SetBinding(target, property, binding); } } }The main window declares a DataGrid containing my custom column:
<DataGrid x:Name="datagrid" ItemsSource="{Binding Employees}" AutoGenerateColumns="False" CanUserAddRows="False"> <DataGrid.Columns> <local:DataGridCommandColumn Binding="{Binding}" Header="Employee" Command="{Binding DataContext.SelectCommand, RelativeSource={RelativeSource AncestorType={x:Type Window}}}" CommandParameter="{Binding}" /> </DataGrid.Columns> </DataGrid>A ViewModel class acts as the Window's DataContext and exposes a list of employees and one ICommand object named SelectCommand. This command expects an Employee instance as a parameter.
public class ViewModel { public ICommand SelectCommand { get; private set; } public List<Employee> Employees { get { return new List<Employee> { new Employee { FirstName = "John", LastName = "Box" }, new Employee { FirstName = "Daniel", LastName = "Tree" }, }; } } public ViewModel() { this.SelectCommand = new RelayCommand(param => Select(param as Employee)); } private void Select(Employee employee) { if (employee == null) return; } } public class Employee { public string FirstName { get; set; } public string LastName { get; set; } public override string ToString() { return string.Format("{0} {1}", this.FirstName, this.LastName); } }What I am trying to do is to bind the menu item selection to the SelectCommand exposed by the ViewModel instance. But The Command property and the CommandParameter property inside my column class are both null. How do I correctly bind these properties?
My sample can be downloaded at:
Thanks!
Michel Miranda
Tutte le risposte
-
martedì 1 maggio 2012 04:52
I briefly looked at your code. The problem is this, the dependencyproperties show up fine, so that part is right, however, they have no meaning because a DataGridTextBoxColumn has no event to kick of the command like a button does. If you look at the buttonbase, you'll see it implements a ICommandSource interface. This allows a button to start the command when it is clicked. I think the first step would be to change the ColumnType to be closer to a button.
How does this command get executed?
<local:DataGridCommandColumn Binding="{Binding}" Header="Employee" Command="{Binding DataContext.SelectCommand, RelativeSource={RelativeSource AncestorType={x:Type Window}}}" CommandParameter="{Binding}" />JP Cowboy Coders Unite!
-
martedì 1 maggio 2012 04:54
-
martedì 1 maggio 2012 05:10
Hi Mr. Javaman II,
Thanks for your response. But I think it has to do with the binding.
Inside my custom column I am adding a contextmenu to the textblock:
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem) { TextBlock textBlock = (TextBlock)base.GenerateElement(cell, dataItem); MenuItem menuItem = new MenuItem { HeaderStringFormat = "Navigate to {0}" }; ApplyBinding(menuItem, MenuItem.HeaderProperty); menuItem.Click += new RoutedEventHandler(menuItem_Click); textBlock.ContextMenu = new ContextMenu(); textBlock.ContextMenu.Items.Add(menuItem); return textBlock; }Inside the menuitem's click event handler the Command is executed:
private void menuItem_Click(object sender, RoutedEventArgs e) { this.Command.Execute(null); }The problem is the binding of the Command property and the CommandArgument property. These are null.
Thank you!
Michel Miranda
-
martedì 1 maggio 2012 13:08Ok so you are saying when you right click on the header that's when you present the contextmenu? When I tested this yesterday I didn't right click.
JP Cowboy Coders Unite!
-
martedì 1 maggio 2012 15:54
This will get you the command binding to the ViewModel. Because you specified it as a dependency property (good) you can bind to it in the designer, but you have to have a reference to the command which is the <local:ViewModel>, a static reference is created under the Window.Resouces section below.
<Window x:Class="_DataGridCommandColumn.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:_DataGridCommandColumn" Title="MainWindow" Height="350" Width="525"> <Window.Resources> <local:ViewModel x:Key="XVM"></local:ViewModel> </Window.Resources> <DockPanel> <Button DockPanel.Dock="Bottom" Content="Select first employee" HorizontalAlignment="Left" CommandParameter="{Binding Employees[0]}" Margin="4" Command="{Binding RelativeSource={RelativeSource Self}}" /> <DataGrid x:Name="datagrid" ItemsSource="{Binding Employees}" AutoGenerateColumns="False" CanUserAddRows="False"> <DataGrid.Columns> <local:DataGridCommandColumn Header="Employee"
Command="{Binding Source={StaticResource XVM}, Path=SelectCommand}" /> </DataGrid.Columns> </DataGrid> </DockPanel> </Window>
JP Cowboy Coders Unite!
- Modificato Mr. Javaman II martedì 1 maggio 2012 15:55
- Proposto come risposta Sheldon _XiaoModerator mercoledì 2 maggio 2012 05:21
- Contrassegnato come risposta Michel Miranda venerdì 18 maggio 2012 05:58
-
martedì 1 maggio 2012 15:57
You can remove this code in MainWindow and set the datacontext in XAML now that there's a static reference to the Viewmodel.
this.DataContext = new ViewModel();
JP Cowboy Coders Unite!
-
martedì 1 maggio 2012 18:11
Hi Mr. Javaman II,
Thank you. I really appreciate it.
Your solution to my problem is a nice workaround, but my application architecture doesn't allow me to create the viewmodel inside the XAML.
I wonder why my original binding doesn't work.
Thanks,
Michel Miranda
-
martedì 1 maggio 2012 21:01
If you can't create what ultimately is a static reference in XAML, which you nicely exposed via the DPs in the MainWindow, what would the command bindings bind? Doesn't make sense. The reason the Bindings worked is that a static instance of the view model was created for that view. It saw the fact that the Viewmodel has an ICommand, the View then allowed you to use it as a binding.
You can try an alternative. Create a static instance of the viewmodel as a private property, when viewmodel is created set it. Then when you reference the command reference it from the static var. and not this.Command...
JP Cowboy Coders Unite!
- Proposto come risposta Sheldon _XiaoModerator mercoledì 2 maggio 2012 05:21
- Proposta come risposta annullata Michel Miranda mercoledì 2 maggio 2012 07:59
- Contrassegnato come risposta Michel Miranda venerdì 18 maggio 2012 05:59
-
mercoledì 2 maggio 2012 08:02
Mr Javaman II, thanks again. When closing this thread I will mark your reply as an answer. It is definitely a solution but it is not applicable to my situation.
Does anyone know a solution for my bindings?
Thanks,
Michel Miranda
-
venerdì 4 maggio 2012 07:24Moderatore
Hi Michel Miranda,
I have not found binding solution, if I come up with a binding solution, I will update this thread.
best regards,
Sheldon _Xiao[MSFT]
MSDN Community Support | Feedback to us
Microsoft
Please remember to mark the replies as answers if they help and unmark them if they provide no help.
-
martedì 8 maggio 2012 17:26
Hi Sheldon _Xiao,
Thanks in advance! I hope you will find an solution.
Michel Miranda
-
venerdì 18 maggio 2012 04:27Moderatore
Hi,
For objects like DataGridTextColumn to bind its properties a common approach is to add a dummy element as a container of the data source. A sample:
<TextBlock x:Name="WorkaroundTextBlock" Text="I'm a dummy TextBlock to hold datacontext for reference"></TextBlock>
<DataGrid x:Name="datagrid" ItemsSource="{Binding Employees}" AutoGenerateColumns="False" CanUserAddRows="False">
<DataGrid.Columns>
<local:DataGridCommandColumn Binding="{Binding}" Header="Employee"
Command="{Binding Source={x:Reference WorkaroundTextBlock},Path=DataContext.SelectCommand}"
/>
</DataGrid.Columns>
</DataGrid>As you've specified the Window's DataContext in code the dummy element gets its DataContext automatically so that can be used by your custom DataGridTextColumn. Certainly you can write a custom control other than TextBlock to make its name makes more sense. (maybe name it DataSourceContainer or something like this)
Allen Chen [MSFT]
MSDN Community Support | Feedback to us
Get or Request Code Sample from Microsoft
Please remember to mark the replies as answers if they help and unmark them if they provide no help.
- Modificato Allen Chen - MSFTMicrosoft Employee, Moderator venerdì 18 maggio 2012 04:28
- Modificato Allen Chen - MSFTMicrosoft Employee, Moderator venerdì 18 maggio 2012 04:29
- Modificato Allen Chen - MSFTMicrosoft Employee, Moderator venerdì 18 maggio 2012 04:32
- Contrassegnato come risposta Michel Miranda venerdì 18 maggio 2012 05:57
- Modificato Allen Chen - MSFTMicrosoft Employee, Moderator venerdì 18 maggio 2012 06:15
- Modificato Allen Chen - MSFTMicrosoft Employee, Moderator venerdì 18 maggio 2012 06:16
-
venerdì 18 maggio 2012 05:15
Hi Allen Chen,
Thank you very much. I tried your solution and it works. I will close this thread.
One last question. Instead of the workaround textblock I assigned a name to my Window:
<Window x:Class="_DataGridCommandColumn.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:_DataGridCommandColumn" Name="wndMain" Title="MainWindow" Height="350" Width="525">Then I reference the DataContext of the Window in the same way as you did:
<DataGrid x:Name="datagrid" ItemsSource="{Binding Employees}" AutoGenerateColumns="False" CanUserAddRows="False"> <DataGrid.Columns> <local:DataGridCommandColumn Binding="{Binding}" Header="Employee" Command="{Binding DataContext.SelectCommand, Source={x:Reference wndMain}}" CommandParameter="{Binding}" /> </DataGrid.Columns> </DataGrid>But that gives me the following error:
Cannot call MarkupExtension.ProvideValue because of a cyclical dependency. Properties inside a MarkupExtension cannot reference objects that reference the result of the MarkupExtension.
It would be nice to use your solution without having to introduce a dummy element. But your solution is definitely applicable!
Thanks,
Michel Miranda
-
venerdì 18 maggio 2012 05:56Moderatore
Hi,
When using x:Reference it's not allowed to refer to the container of the object. In xaml the Window contains the DataGridCommandColumn so cannot be used as the source. You have to add a dummy element to achieve the goal.
Allen Chen [MSFT]
MSDN Community Support | Feedback to us
Get or Request Code Sample from Microsoft
Please remember to mark the replies as answers if they help and unmark them if they provide no help.
- Modificato Allen Chen - MSFTMicrosoft Employee, Moderator venerdì 18 maggio 2012 05:57
- Modificato Allen Chen - MSFTMicrosoft Employee, Moderator venerdì 18 maggio 2012 05:58
- Contrassegnato come risposta Michel Miranda venerdì 18 maggio 2012 06:01

