Как связать DataGridColumn.Visibility?
У меня есть проблема, похожая на следующий пост:
Видимость привязки Silverlight DataGridTextColumn
Мне нужно, чтобы столбец в DataGrid Silverlight был виден / свернут на основе значения в ViewModel. Для этого я пытаюсь привязать свойство Visibility к 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 также.
У кого-нибудь есть идеи, почему это не работает?
9 ответов
Вот решение, которое я придумал, используя небольшой взлом.
Во-первых, вам нужно наследовать от 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: Name of 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"};
}
Чтобы отобразить, вам нужно только удалить соответствующее имя из списка или воссоздать его без скрытого имени.
У меня есть другое решение этой проблемы, которое использует подход, аналогичный свойству "Binding", которое вы найдете в 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"));
}
}
}
}
}
Столбец сетки данных наследуется от DependencyObject вместо FrameworkElement. В WPF это не составило бы труда... но в silverlight вы можете связывать только объекты FrameworkElement. Таким образом, при попытке получить сообщение об ошибке AG_E_PARSER_BAD_PROPERTY_VALUE.
Я не знаю, насколько это поможет, но я столкнулся с проблемой отсутствия свойства зависимости со столбцами сетки данных в своем последнем проекте. Чтобы обойти это, я создал событие в модели представления столбцов сетки, затем, когда сетка собирается в клиенте, используйте замыкание, чтобы подписать столбец сетки на модель представления столбцов. Моя конкретная проблема была вокруг ширины. Он начинается с класса модели представления для столбца сетки, который выглядит примерно так: псевдокод:
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, которое вызывает метод resize в модели представления, к которой привязана сетка. Это, в свою очередь, вызывает метод изменения размера каждой модели представления столбца сетки. Модель представления столбца сетки затем поднимает ColumnResized
событие, на которое подписан текстовый столбец сетки данных, и его ширина обновляется.
Я понимаю, что это не решает вашу проблему напрямую, но это был способ, которым я мог "привязать" модель представления к столбцу таблицы данных, когда у него нет свойств зависимости. Закрытие - это простая конструкция, которая прекрасно описывает желаемое поведение и вполне понятна для тех, кто идет за мной. Я думаю, не сложно представить, как его можно изменить, чтобы справиться с изменением видимости. Вы даже можете подключить обработчик событий к событию загрузки страницы / пользовательского элемента управления.
Это работает для столбца шаблона сетки данных:
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;
}
}
}
Решение 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"));
}
Крис Манчини,
Вы не создаете привязку к свойству "Связывание" столбца сетки данных. Хорошо, вы пишете "{Binding User.UserName}", но он не создает привязку, потому что (как сказал Захарий) столбец сетки данных не наследуется от FrameworkElement и не имеет метода SetBinding. Поэтому выражение "{Binding User.UserName}" просто создает объект Binding и присваивает его свойству Binding столбца (это свойство типа Binding). Затем столбец таблицы данных при создании содержимого ячеек (метод GenerateElement - защищенный) использует этот объект Binding для установки привязки к сгенерированным элементам (например, к свойству Text сгенерированного TextBlock), которые являются FrameworkElements
Обратите внимание, что проблема не просто в том, что "Видимость" не является свойством зависимости. В 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/
И пара вопросов Stackru:
Из вашего класса MyDataGridTextColumn вы можете получить окружающий DataGrid. Затем вы получаете свою ViewModel из DataContext DataGrid и добавляете обработчик к событию PropertyChanged вашей ViewModel. В обработчике вы просто проверяете имя свойства и его значение и соответственно изменяете видимость столбца. Это не совсем лучшее решение, но оно должно работать;)