Con risposta DataGrid, Implementing a custom command column

  • lunedì 30 aprile 2012 17:45
     
      Contiene codice

    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:

    https://skydrive.live.com/?qt=shared&cid=219633fed3c94c95#cid=219633FED3C94C95&id=219633FED3C94C95%21117

    Thanks!

    Michel Miranda

Tutte le risposte

  • martedì 1 maggio 2012 04:52
     
      Contiene codice

    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
     
      Contiene codice

    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:08
     
     
    Ok 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
     
     Con risposta Contiene codice

    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!


  • martedì 1 maggio 2012 15:57
     
      Contiene codice

    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
     
     Con risposta

    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!

  • 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:24
    Moderatore
     
     

    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:27
    Moderatore
     
     Con risposta

    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.






  • venerdì 18 maggio 2012 05:15
     
      Contiene codice

    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:56
    Moderatore
     
     Con risposta

    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.