Проблема с привязкой пользовательского столбца DataGridColumn в WPF

#c# #wpf #binding #datagrid

#c# #wpf #привязка #datagrid

Вопрос:

Я пытаюсь определить новый шаблон столбца для datagrid, который я могу повторно использовать в своем приложении, но когда я пытаюсь его использовать, я получаю:

Система.Windows.Ошибка данных: 2: Не удается найти управляющий FrameworkElement или FrameworkContentElement для целевого элемента. Выражение привязки: Path= CanLogin; DataItem= null; целевой элемент — ‘DataGridBetterCheckBoxColumn’ (хэш-код = 56040243); целевое свойство — ‘IsChecked’ (тип ‘Object’)

XAML для столбца:

 <DataGridTemplateColumn x:Class="BACSFileGenerator.UserControls.DataGridBetterCheckBoxColumn"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:BACSFileGenerator.UserControls"
             mc:Ignorable="d" 
             x:Name="ColumnRoot"
             >
    <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
            <CheckBox IsChecked="{Binding isChecked, Source={x:Reference Name=ColumnRoot}}"/>
        </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
  

Код, лежащий в основе:

 using System.Windows;
using System.Windows.Controls;

namespace BACSFileGenerator.UserControls
{

    public partial class DataGridBetterCheckBoxColumn : DataGridTemplateColumn
    {

        public object isChecked
        {
            get { return (object)GetValue(isCheckedProperty); }
            set { SetValue(isCheckedProperty, value); }
        }

        public static readonly DependencyProperty isCheckedProperty =
            DependencyProperty.Register("isChecked", typeof(object),
              typeof(DataGridBetterCheckBoxColumn), new PropertyMetadata(null));

        public DataGridBetterCheckBoxColumn()
        {
            InitializeComponent();
        }
    }
}
  

Затем я пытаюсь использовать его следующим образом:

 <DataGrid Margin="0,0,0,10" ItemsSource="{Binding UserAccessGrid}" CanUserAddRows="False" CanUserDeleteRows="False" AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridTextColumn Header="User" Binding="{Binding User}" IsReadOnly="True"/>
                <uc:DataGridBetterCheckBoxColumn Header="Login" isChecked="{Binding CanLogin}"/>
                <uc:DataGridBetterCheckBoxColumn Header="Export Payments" isChecked="{Binding canExportPayments}"/>
                <uc:DataGridBetterCheckBoxColumn Header="Create File Layouts" isChecked="{Binding canCreateFileLayouts}"/>
                <uc:DataGridBetterCheckBoxColumn Header="Change User Access" isChecked="{Binding canChangeUserAccess}"/>
            </DataGrid.Columns>
</DataGrid>
  

Кто-нибудь может объяснить мне, как правильно это сделать?

Ответ №1:

Допустим, у нас есть

 public class ViewModel
{
   public bool CanBeUsed {get;set;}
   public List<Employee> Employees{get;set;}
}
  

Несколько моментов, которые могли вас смутить :

  1. Для свойства будет создан только один DataGridBetterCheckBoxColumn экземпляр. Несколько записей не означают несколько экземпляров столбца для свойства. Вместо DataGridCell этого для каждого создается несколько DataGridColumn .

    Но

    DataGridColumn это не a FrameworkElement или Visual so, оно не будет отображаться в VisualTree , и, поскольку это не FrameworkElement так, у него нет DataContext свойства. Без DataContext того, как вы Binding будете работать? Спросите себя. Поскольку у этого Column не может быть своего DataContext набора, поэтому он должен иметь либо a ElementName , либо a Source , либо a RelativeSource для его Binding работы.

    Теперь мы знаем, что будет только один экземпляр a DataGridColumn , поэтому, естественно, он Binding должен (должен) использовать DataContext (свойство коллекции будет частью этого) of DataGrid .

    Теперь посмотрите Binding , где находится его Source / RelativeSource ? Их нет. Теперь, будет RelativeSource ли здесь какой-то смысл? As DataGridColumn не отображается в VisualTree , поэтому RelativeSource здесь не применяется. У нас осталось Source свойство. Что мы должны установить сейчас для Source ? Введите DataContext Inheritance .

    Наследование DataContext

    DataContext наследование будет работать только для FrameworkElement подключенных через VisualTree . Итак, нам нужен механизм, с помощью которого мы можем перенести это DataContext на наш DataGridColumn . Введите Binding Proxy .

     public class BindingProxy : Freezable
    {
        #region Overrides of Freezable
    
        protected override Freezable CreateInstanceCore()
        {
             return new BindingProxy();
        }
    
        #endregion
    
        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));
     }
      

    Если мы объявим экземпляр этого BindingProxy как a Resource , мы можем получить наш Source .

     <DataGrid Margin="0,52,0,10" ItemsSource="{Binding Records}" CanUserAddRows="False" CanUserDeleteRows="False" AutoGenerateColumns="False">
       <DataGrid.Resources>
         <uc:BindingProxy x:Key="FE" Data="{Binding}"/>
       </DataGrid.Resources>
       <DataGrid.Columns>
          <DataGridTextColumn x:Name="dgt" Header="User" Binding="{Binding User}" IsReadOnly="True"/>
          <uc:DataGridBetterCheckBoxColumn isChecked="{Binding Data.CanBeUsed, Source={StaticResource FE}}" Header="CanLogin"/>
       </DataGrid.Columns>
    </DataGrid>
      

    Теперь вы увидите, что ваша неприятность Binding Error исчезла.

  2. Чтобы ваша CheckBox привязка работала должным образом, вам необходимо обработать ее Loaded событие.

       <DataGridTemplateColumn.CellTemplate>
         <DataTemplate>
           <CheckBox Loaded="CheckBox_Loaded"/>
         </DataTemplate>      
      </DataGridTemplateColumn.CellTemplate>
      

    Код :

       void CheckBox_Loaded(object sender, RoutedEventArgs e)
      {
          Binding b = new Binding();
          b.Path = new PropertyPath("isChecked");
          b.Mode = BindingMode.TwoWay;
          b.Source = this;
    
          CheckBox cb = sender as CheckBox;
    
          BindingOperations.SetBinding(cb , CheckBox.IsCheckedProperty, b);
      }
      

    Но теперь у нас есть одна логическая проблема. Все наши CheckBox теперь ограничены DataContext свойством CanBeUsed , которое останется неизменным. Возможно, вы думаете, что CanBeUsed это должно быть свойство Employee , которое является ItemsSource , а не DataContext of DataGrid . Итак, когда вы устанавливаете / снимаете флажок any CheckBox , все будут отвечать одинаково.

Но мы хотим привязать наше isChecked свойство к некоторому свойству Employee записи, которое будет оставаться diff для каждого DataGridRow . Итак, теперь нам нужно изменить наше определение isChecked , после чего весь код будет выглядеть следующим образом :

 public partial class DataGridBetterCheckBoxColumn : DataGridTemplateColumn
{
    public BindingBase isChecked { get; set; }

    public DataGridBetterCheckBoxColumn()
    {
        InitializeComponent();
    }

    void CheckBox_Loaded(object sender, RoutedEventArgs e)
    {          
        CheckBox cb = sender as CheckBox;

        BindingOperations.SetBinding(cb , CheckBox.IsCheckedProperty, isChecked);
    }
} 
  

Использование :

 <uc:DataGridBetterCheckBoxColumn isChecked="{Binding CanLogin, Mode=TwoWay}" Header="CanLogin"/>
  

Если я пропустил какой-либо момент, дайте мне знать.