Как связать DataGridColumn.Visibility?

У меня проблема, похожая на следующую запись:

Silverlight DataGridTextColumn Binding Visibility

Мне нужно, чтобы столбец в Silverlight DataGrid был виден/свернут на основе значения в ViewModel. Для этого я пытаюсь привязать свойство видимости к ViewModel. Однако вскоре я обнаружил, что свойство Visibility не является DependencyProperty, поэтому оно не может быть связано.

Чтобы решить эту проблему, я попытался подклассифицировать мою собственную DataGridTextColumn. С этим новым классом я создал DependencyProperty, который в конечном итоге подталкивает изменения к свойству DataGridTextColumn.Visibility. Это хорошо работает, если я не привяжусь к данным. В тот момент, когда я привязываюсь к своему новому свойству, он терпит неудачу с исключением AG_E_PARSER_BAD_PROPERTY_VALUE.

public class MyDataGridTextColumn : DataGridTextColumn
{
    #region public Visibility MyVisibility

    public static readonly DependencyProperty MyVisibilityProperty =
        DependencyProperty.Register("MyVisibility", typeof(Visibility), typeof(MyDataGridTextColumn), new PropertyMetadata(Visibility.Visible, OnMyVisibilityPropertyChanged));

    private static void OnMyVisibilityPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var @this = d as MyDataGridTextColumn;

        if (@this != null)
        {
            @this.OnMyVisibilityChanged((Visibility)e.OldValue, (Visibility)e.NewValue);
        }
    }

    private void OnMyVisibilityChanged(Visibility oldValue, Visibility newValue)
    {
        Visibility = newValue;
    }

    public Visibility MyVisibility
    {
        get { return (Visibility)GetValue(MyVisibilityProperty); }
        set { SetValue(MyVisibilityProperty, value); }
    }

    #endregion public Visibility MyVisibility
}

Вот небольшой фрагмент XAML.

<DataGrid ....>
    <DataGrid.Columns>
        <MyDataGridTextColumn Header="User Name"
                              Foreground="#FFFFFFFF"
                              Binding="{Binding User.UserName}"
                              MinWidth="150"
                              CanUserSort="True"
                              CanUserResize="False"
                              CanUserReorder="True"
                              MyVisibility="{Binding Converter={StaticResource BoolToVisibilityConverter}, Path=ShouldShowUser}"/>
        <DataGridTextColumn .../>
    </DataGrid.Columns>
</DataGrid>

Несколько важных фактов.

  • Конвертер действительно определен выше в локальных ресурсах.
  • Конвертер верен, он используется во многих других местах в решении.
  • Если я заменил синтаксис {Binding} свойства MyVisibility на "Collapsed", столбец действительно исчезнет.
  • Если я создаю новый DependencyProperty (т.е. строку Foo) и привяжу к нему, я получаю также исключение AG_E_PARSER_BAD_PROPERTY_VALUE.

Есть ли у кого-нибудь идеи, почему это не работает?

Ответы

Ответ 1

Вот решение, которое я придумал с помощью небольшого взлома.

Во-первых, вам нужно наследовать от DataGrid.

public class DataGridEx : DataGrid
{
    public IEnumerable<string> HiddenColumns
    {
        get { return (IEnumerable<string>)GetValue(HiddenColumnsProperty); }
        set { SetValue(HiddenColumnsProperty, value); }
    }

    public static readonly DependencyProperty HiddenColumnsProperty =
        DependencyProperty.Register ("HiddenColumns", 
                                     typeof (IEnumerable<string>), 
                                     typeof (DataGridEx),
                                     new PropertyMetadata (HiddenColumnsChanged));

    private static void HiddenColumnsChanged(object sender,
                                             DependencyPropertyChangedEventArgs args)
    {
        var dg = sender as DataGrid;
        if (dg==null || args.NewValue == args.OldValue)
            return;

        var hiddenColumns = (IEnumerable<string>)args.NewValue;
        foreach (var column in dg.Columns)
        {
            if (hiddenColumns.Contains ((string)column.GetValue (NameProperty)))
                column.Visibility = Visibility.Collapsed;
            else
                column.Visibility = Visibility.Visible;
        }
    }
}

Класс DataGridEx добавляет новый DP для скрытия столбцов на основе x: Имя DataGridColumn и его потомков.

Для использования в вашем XAML:

<my:DataGridEx x:Name="uiData"
               DataContext="{Binding SomeDataContextFromTheVM}"
               ItemsSource="{Binding Whatever}"
               HiddenColumns="{Binding HiddenColumns}">
    <sdk:DataGridTextColumn x:Name="uiDataCountOfItems">
                            Header="Count"
                            Binding={Binding CountOfItems}"
    </sdk:DataGridTextColumn>
</my:DataGridEx>

Вам нужно добавить их в свою ViewModel или любой другой контекст данных, который вы используете.

private IEnumerable<string> _hiddenColumns;
public IEnumerable<string> HiddenColumns
{
    get { return _hiddenColumns; }
    private set
    {
        if (value == _hiddenColumns)
            return;

        _hiddenColumns = value;
        PropertyChanged (this, new PropertyChangedEventArgs("HiddenColumns"));
    }
}

public void SomeWhereInYourCode ()
{
    HiddenColumns = new List<string> {"uiDataCountOfItems"};
}

Чтобы отобразить, вам нужно только удалить соответствующее имя из списка или воссоздать его без скрытого имени.

Ответ 2

У меня есть другое решение этой проблемы, которое использует подход, похожий на свойство "Связывание", которое вы найдете в DataGridTextColumn. Поскольку классы столбцов являются DependencyObjects, вы не можете напрямую привязывать к ним данные, но если вы добавите ссылку на элемент FrameworkElement, который реализует INotifyPropertyChanged, вы можете передать привязку данных к элементу, а затем использовать свойство зависимости, чтобы уведомить столбец, что привязка данных изменилась.

Следует отметить, что привязка к самому столбцу вместо Grid будет означать, что вы захотите использовать DataContextProxy чтобы получить доступ к полю, к которому вы хотите привязать видимость (привязка столбцов по умолчанию будет обладать параметром ItemSource).

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace XYZ.Controls
{
public class ExtendedDataGridTextColumn : DataGridTextColumn
{
    private readonly Notifier _e;

    private Binding _visibilityBinding;
    public Binding VisibilityBinding
    {
        get { return _visibilityBinding; }
        set
        {
            _visibilityBinding = value;
            _e.SetBinding(Notifier.MyVisibilityProperty, _visibilityBinding);
        }
    }

    public ExtendedDataGridTextColumn()
    {
        _e = new Notifier();
        _e.PropertyChanged += ToggleVisibility;
    }

    private void ToggleVisibility(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == "Visibility")
            this.Visibility = _e.MyVisibility;
    }

    //Notifier class is just used to pass the property changed event back to the column container Dependency Object, leaving it as a private inner class for now
    private class Notifier : FrameworkElement, INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public Visibility MyVisibility
        {
            get { return (Visibility)GetValue(MyVisibilityProperty); }
            private set { SetValue(MyVisibilityProperty, value); }
        }

        public static readonly DependencyProperty MyVisibilityProperty = DependencyProperty.Register("MyVisibility", typeof(Visibility), typeof(Notifier), new PropertyMetadata(MyVisibilityChanged));

        private static void MyVisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var n = d as Notifier;
            if (n != null)
            {
                n.MyVisibility = (Visibility) e.NewValue;
                n.PropertyChanged(n, new PropertyChangedEventArgs("Visibility"));
            }
        }
    }
}

}

Ответ 3

Столбец datagrid наследует от DependencyObject вместо FrameworkElement. В WPF это было бы неважно... но в silverlight вы можете привязываться только к объектам FrameworkElement. Таким образом, вы получите сообщение об ошибке AG_E_PARSER_BAD_PROPERTY_VALUE при попытке.

Ответ 4

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

public delegate void ColumnResizedEvent(double width);

public class GridColumnViewModel : ViewModelBase
{
    public event ColumnResizedEvent ColumnResized;

    public void Resize(double newContainerWidth)
    {
        // some crazy custom sizing calculations -- don't ask...
        ResizeColumn(newWidth);
    }

    public void ResizeColumn(double width)
    {
        var handler = ColumnResized;
        if (handler != null)
            handler(width);
    }
}

Затем появится код, который собирает сетку:

public class CustomGrid
{
    public CustomGrid(GridViewModel viewModel)
    {
        // some stuff that parses control metadata out of the view model.
        // viewModel.Columns is a collection of GridColumnViewModels from above.
        foreach(var column in viewModel.Columns)
        {
            var gridCol = new DataGridTextColumn( ... );
            column.ColumnResized  += delegate(double width) { gridCol.Width = new DataGridLength(width); };
        }
    }
}

Когда размер файла данных изменяется в приложении, событие resize выбирается и вызывает метод изменения размера в режиме просмотра, к которому привязана сетка. Это, в свою очередь, вызывает метод изменения размера каждой модели представления столбцов сетки. Затем модель представления столбцов сетки вызывает событие ColumnResized, к которому привязан столбец текста сетки данных, и его ширина обновляется.

Я понимаю, что это прямо не решает вашу проблему, но я мог "привязать" модель представления к столбцу сетки данных, если на нем нет свойств зависимостей. Закрытие представляет собой простую конструкцию, которая красиво инкапсулирует поведение, которое я хотел, и вполне понятно, кто идет за мной. Я думаю, что не сложно представить, как это можно изменить, чтобы справиться с изменением видимости. Вы даже можете подключить обработчик событий в событии загрузки элемента управления страницы/пользователя.

Ответ 5

Крис Манчини,

вы не создаете привязку к свойству "Связывание" столбца сетки данных. Ну, вы пишете "{Binding User.UserName}", но он не создает привязки, потому что (как сказал zachary) столбец datagrid не наследует от FrameworkElement и не имеет метода SetBinding. Поэтому выражение "{Binding User.UserName}" просто создает объект Binding и присваивает ему свойство Binding столбца (это свойство является типом привязки). Затем столбец datagrid при создании содержимого ячеек (метод, защищенный GenerateElement) использует этот объект Binding для установки привязки на сгенерированные элементы (например, на свойство Text сгенерированного TextBlock), которые являются FrameworkElements

Ответ 6

Отличное решение GreatTall1, но для его работы необходимо немного изменить бит.

var n = d as Notifier;
if (n != null)
{
     //Assign value in the callback will break the binding.
     //n.MyVisibility = (Visibility)e.NewValue;
     n.PropertyChanged(n, new PropertyChangedEventArgs("Visibility"));
}

Ответ 7

Обратите внимание, что проблема не так проста, как "Видимость", не являющаяся свойством зависимости. В DataGrid столбцы не являются частью визуального "дерева", поэтому вы не можете использовать AncestorType даже в WPF (или Silverlight 5).

Вот несколько ссылок, связанных с WPF (просьба прокомментировать, если какая-либо из этих работ для Silverlight - извините, у меня нет времени на тестирование сейчас)

Имеет действительно хорошее объяснение проблемы и сбоев определенных решений (и умное решение): http://tomlev2.wordpress.com/2011/03/21/wpf-how-to-bind-to-data-when-the-datacontext-is-not-inherited/

И несколько вопросов StackOverflow:

WPF Скрыть DataGridColumn через привязку

Связывание Видимое свойство DataGridColumn в WPF DataGrid

Ответ 8

Это работает в столбце шаблона сетки данных:

public class ExtendedDataGridColumn : DataGridTemplateColumn
{
    public static readonly DependencyProperty VisibilityProperty = DependencyProperty.Register("Visibility", typeof(Visibility), typeof(DataGridTemplateColumn), new PropertyMetadata(Visibility.Visible, VisibilityChanged));
    public new Visibility Visibility
    {
        get { return (Visibility)GetValue(VisibilityProperty); }
        set { SetValue(VisibilityProperty, value); }
    }
    private static void VisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if ((DataGridTemplateColumn)d != null)
        {
            ((DataGridTemplateColumn)d).Visibility = (Visibility)e.NewValue;
        }
    }
}

Ответ 9

Из класса MyDataGridTextColumn вы можете получить окружение DataGrid. Затем вы получите свой ViewModel из DataContext DataGrid и добавьте обработчик события PropertyChanged вашего ViewModel. В обработчике вы просто проверяете имя свойства и его значение и соответственно изменяете видимость столбца. Это не совсем лучшее решение, но оно должно работать;)