Pull to refresh

Трудный выбор грида для проектов на WPF

Reading time 21 min
Views 62K
Перед нашей командой встала задача выбора для будущих проектов библиотеки WPF компонентов для быстрой разработки пользовательского интерфейса бизнес-приложений. Посылом к рассмотрению стало то, что стандартный грид (а это обычно большая часть функционала пользовательского интерфейса бизнес-приложения) не устраивает по многим параметрам. Много чего приходится допиливать напильником, изобретая очередной велосипед. Надоело! Мы решили сравнить гриды из каждой библиотеки и сделать обоснованный выбор.

Участники конкурса


Думаю, что список участников сравнения никого не удивит:

Кому интересно как это происходило, прошу под кат.

Стоит сказать, что у нас в команде уже имеется опыт работы с наборами WPF компонентов от Telerik и DevExpress. Так же имеется опыт работы с гридом WinForms десятилетней выдержки от ComponentOne. Я сам использую WPF для коммерческих проектов с 2009 года и, конечно же, успел написать свой грид-велосипед и MVVM фреймворк.

Как сравнивали


Сразу скажу, что сравнение таких материй, как «удобство разработки», «скорость реализации фичи» и т.д. является в известной степени субъективным. Поэтому в некоторых субъективных суждениях я не претендую на истину в последней инстанции и высказываю свое видение этого вопроса.

Изначально четкого списка необходимого функционала не было, потому что сравнивать все 5 гридов по большому перечню функционала будет очень ресурсоемко. Поэтому первым этапом был поверхностный субъективный взгляд на сам грид в шаблонной задаче и впечатления в субъективных категориях «нравится»/«не нравится».

На первом этапе отпали первые два кандидата, и мы стали подробно смотреть трех оставшихся, предварительно составив список необходимого функционала. Далее опишу процесс исследования и свои впечатления о каждом гриде.

DevExpress


Впечатления о гриде сильно отрицательные. Такое ощущение, что он целиком скопирован из WinForms с изменением пространств имен. По стилю компоненты повторяют WinForms, что с одной стороны хорошо для тех, кто только пересаживается на WPF с WinForms, а с другой по факту нет перехода к новой технологии. Субъективно сложно после 4 лет разработки на WPF пересаживаться на этот грид. Приходится искать описание каждого свойства и метода (хотя документация у них хороша). Например, выбранная строка в гриде это не SelectedItem, как подумают многие WPF разработчики, а, внимание, FocusedRow. Очевидно, не спорю, но уж очень непривычно.

Биндинг в колонки, кастомизация колонок

Биндинг колонок работает хорошо. Как в оригинальном WPF. Редактирование в гриде не напрягает и все просто работает. Кастомизация так же сделана на высоком уровне: присутствуют стили, шаблоны и селекторы шаблонов для ячеек.

Раскраска строк через одну

Но вот дальше началось. Простейшие операции, которые в оригинальном WPF гриде выполняются выставлением одного свойства, в гриде от DevExpress влекут за собой километровые полотна кода. Например, что может быть проще, чем раскрасить строки грида через одну: нечетная – белая, четная – серая. Для тех, кто не знаком с WPF скажу, что в оригинальном гриде «из коробки» .NET Framework это делается так:
<DataGrid AlternatingRowBackground="LightGray"/>

А вот у разработчиков DevExpress другой взгляд на жизнь и у них такого свойства нет. Официальный пример от разработчиков гласит, что надо делать так:
<!--Цвет четной строки в гриде-->
<SolidColorBrush x:Key="EvenRowBrush" Color="LightGray" />
<!--Цвет нечетной строки в гриде-->
<SolidColorBrush x:Key="OddRowBrush" Color=" Transparent" />
<!--Стиль для раскраски четных строк в гриде-->
<Style BasedOn="{StaticResource {dxgt:GridRowThemeKey ThemeName=Seven, ResourceKey=RowStyle}}" TargetType="{x:Type dxg:GridRowContent}">
    <Style.Triggers>
        <MultiDataTrigger>
            <MultiDataTrigger.Conditions>
                <Condition Binding="{Binding Path=EvenRow}" Value="False"/>
                <Condition Binding="{Binding Path=SelectionState}" Value="None"/>
            </MultiDataTrigger.Conditions>
            <Setter Property="Background" Value="{StaticResource EvenRowBrush}" />
        </MultiDataTrigger>
        <MultiDataTrigger>
            <MultiDataTrigger.Conditions>
                <Condition Binding="{Binding Path=EvenRow}" Value="True"/>
                <Condition Binding="{Binding Path=SelectionState}" Value="None"/>
            </MultiDataTrigger.Conditions>
            <Setter Property="Background" Value="{StaticResource OddRowBrush}" />
        </MultiDataTrigger>
    </Style.Triggers>
</Style>

Странно, не понятно и напрягает, что приходится гуглить такие простейшие вещи и получать такие нетривиальные решения.

Мастер-деталь

Следующим сценарием, который мы смотрели на гриде, был создание связки мастер-деталь со вложенным гридом. Т.е. я хочу, чтобы в таблице были плюсики, по клику в которых будут открываться вложенные таблицы со связанными сущностями. Например, заказы и строки заказов.

Было найдено несколько примеров реализации такого функционала, каждый из которых обладал своими недостатками и достоинствами. Например, вот этот пример ограничивает высоту деталей в 300 пикселей. А если у меня строк будет больше? Тогда появится второй скрол, рядом с основным? Некрасиво и неудобно.

Есть другой вариант реализации мастер-детали, используя DataControlDetailDescriptor. В коде выглядит вот так:
<dxg:GridControl.DetailDescriptor>
    <dxg:DataControlDetailDescriptor ItemsSourcePath="Children">
        <dxg:DataControlDetailDescriptor.DataControl>
            <dxg:GridControl>
                <dxg:GridControl.View >
                    <dxg:TableView>
                    </dxg:TableView>
                </dxg:GridControl.View>
                <dxg:GridControl.Columns>
                </dxg:GridControl.Columns>
            </dxg:GridControl>
        </dxg:DataControlDetailDescriptor.DataControl>
    </dxg:DataControlDetailDescriptor>
</dxg:GridControl.DetailDescriptor>

Откуда такая дикая вложенность тегов? Я понимаю, что xml – это убогий, ни разу не компактный, формат, но не до такой же степени! Забегая вперед скажу, что у конкурентов в этом плане все лаконичней.

Более того, второй вариант приводит к тому, что вложенный грид с деталями практически не отделен от мастер строк, а точнее последняя строка деталь грида не отделена от следующей строки мастер грида. Это может привести пользователя в замешательство. При этом явное выставление границ у деталь грида не дало никаких результатов.

Так же, при таком подходе биндинг свойства FocusedRow у деталь грида не работает! Также невозможно достаточно просто определить какой грид находится в фокусе, чтобы менять доступность команд на панели инструментов. Пришлось играться множеством связанных событий изменения фокуса двух гридов и выводить из этой последовательности, какой же грид на самом деле в фокусе и какие команды должны быть доступны.

Биндинг команд контекстного меню

Биндинг команд в контектсном меню просто отвратительный. Нарушено наследование свойства DataContext при формировании контектсного меню у грида. В DataContext элемента контекстного меню лежит экземпляр какого-то служебного класса и чтобы привязать команду из ViewModel необходимо писать такую строчку:
Command="{Binding Path=View.DataContext.CancelCommand}"

Причем в процессе поиска такой правильной последовательности свойств было столько различных вариантов. Приведенный вариант был найден не через один час поисков.

Вердикт

К вышеперечисленным недостаткам можно добавить зависания и падения визуального редактора студии, когда в нем открыты компоненты DevExpress.

На этом наши эксперименты с DevExpress закончились. Но надо сказать, что все вышеперечисленные проблемы с гридом от DevExpress – это практически полностью субъективная оценка и для человека, разрабатывавшего с использованием комопнентов WinForms от DevExpress, все будет боле радужно и привычно.

Так же нельзя не упомянуть о таком крутом функционале, как редактирование расположения компонентов на форме пользователем с сохранением настроек между запусками. Это действительно полезный функционал, особенно в крупных проектах, с которыми работают продвинутые пользователи.

Infragistics NetAdvantage


Следующим подопытным был грид от компании Infragistics. К своему удивлению я обнаружил сразу два грида: XamGrid и XamDataGrid. Функциональное насыщение каждого из гридов странное и описано здесь.

А если прикладная задача потребуют функционал обоих гридов в одном? Странно, ну да ладно, будем пробовать. Для примера возьмем XamDataGrid, который кажется более богатым по функционалу и возможностям кастомизации.

Биндинг в колонки

Первое что смутило, так это то, что у грида нет свойства Columns. Есть некое свойство FieldLayouts, которое объявляется вот так:
<igDP:XamDataGrid AutoFit="True" DataSource="{Binding Constituents}">
    <igDP:XamDataGrid.FieldLayouts>
    <igDP:FieldLayout>
        <igDP:FieldLayout.Fields>
        <igDP:Field Label="Name" Name="Name" />
        <igDP:Field Label="Photo" Name="ImageUri">…</igDP:Field>
        <igDP:Field Label="Age" Name="Age" />
        </igDP:FieldLayout.Fields>
    </igDP:FieldLayout>
    </igDP:XamDataGrid.FieldLayouts>
</igDP:XamDataGrid>

Странно. Так же нельзя просто взять и задать биндинг. Только наименование поля. А если мы хотим обратиться к полю связанного объекта, то надо делать так:
<igDP:UnboundField Name="StreetDesc" Label="Security Description">
    <igDP:Field.Settings>
        <igDP:FieldSettings>
            <igDP:FieldSettings.CellValuePresenterStyle>
                <Style TargetType="igDP:CellValuePresenter">
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate>
                                <TextBlock DataContext="{Binding DataItem.Security}" Text="{Binding StreetDesc}"/>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </igDP:FieldSettings.CellValuePresenterStyle>
        </igDP:FieldSettings>
    </igDP:Field.Settings>
</igDP:UnboundField>


Вердикт

Нет, это не WPF! Решили не тратить дальше время на исследование этого монстра и отложили его.

Xceed


Дальше все интересней. Если по первым двум гридам впечатления отрицательные, то про этот нельзя так сказать. С одной стороны грид хорошо написан, есть много интересного, действительно быстрый при больших объемах данных. С другой стороны разработчики не парились такими вопросами как локализация приложений. Но давайте по порядку.

Ленивая догрузка записей и поддержка WCF DataServices

Этот грид я почему-то начал смотреть с производительности и возможности интеграции с WCF DataServices (потому что именно эта технология у нас выбрана для связи с сервером приложений). Странный выбор функционала для первого взгляда на грид, согласен, но к тому моменту устал смотреть примитивы и решил поразвлечься. На удивление грид работает быстро, есть встроенная поддержка WCF DataServices. Можно в источник данных подсунуть IQueryable и грид сам реализует примитивную серверную фильтрацию и сортировку с ленивой дочиткой записей на клиента при скроллинге. Работает потрясающе. Я был в восторге, что такое возможно, выставлением одного свойства.

Но дальше пришло разочарование. Да, фильтры работают, но только для полей объекта, который непосредственно показывается в гриде. По полям связанных объектов он уже не умеет ни фильтровать, ни сортировать. И если автоматическое поле фильтрации просто заблокировано, то при сортировке он падает со странным исключением. Да и вообще стандартные компоненты для фильтрации в гриде разочаровали. Для сравнения у Infragistics это прокачано гораздо лучше и правильней. Но фильтры можно кастомизировать и поэтому не так критично.

Биндинг в колонки

Вот тут пришло разочарование. Оказывается, нельзя задать выражение биндинга для колонки грида. Можно указать значение свойства FieldName. В дополнение к нему есть свойство DisplayMemberBindingInfo, в котором можно задавать привычные для биндинга свойства (но не все), но оно не приводимо из расширения разметки (нельзя просто взять и написать DisplayMemberBindingInfo="{Binding …}"). При этом в DataContext ячейки попадет не экземпляр объекта данных, а экземпляр связанного свойства объекта данных. Вроде логично, но как тогда в стиле/шаблоне ячейки достучаться к соседним колонкам или другим свойствам объекта данных? Оказывается можно, приплясывая с таким бубном:
<xcdg:Column FieldName="Product.Name" Title="Товар"
                CellContentTemplateSelector="{StaticResource ProductTemplateSelector}">
    <xcdg:Column.DisplayMemberBindingInfo>
        <xcdg:DataGridBindingInfo ReadOnly="True" Path="."/>
    </xcdg:Column.DisplayMemberBindingInfo>
</xcdg:Column>

Здесь важным является то, что при выставлении значения Path="." обязательно указывать ReadOnly=«True». Это позволит в DataContext ячейки положить экземпляр объекта данных (Order), а не его свойства (Product.Name).

Мне не понятно, зачем надо было делать именно так. Ну да ладно. С этим можно было бы жить, но дальше я столкнулся с потрясающим поведением. Пытливый читатель наверняка уже негодует и пишет комментарий в стиле «в примере колонки присвоение свойства FieldName=«Product.Name» лишнее, оно не несет никакого смысла». Согласен, давайте удалим. В результате мы огребем от компилятора такую ошибку:
Value cannot be null. Parameter name: key
Какое значение, какой параметр key? Не понятно. При этом попытка создать две колонки с одинаковым значением свойства FieldName выдает вот такую ошибку:
A column with same field name already exists in collection.
Ага, уже теплее. Тут меня разобрало любопытство, и я полез рефлектором в исходники. Оказывается, что сама коллекция Columns хоть и является потомком ObservableCollection, но внутри (видимо для оптимизации доступа) содержит приватное поле:
Dictionary<string, ColumnBase> m_fieldNameToColumns;

Получается, что мы должны придумывать мифическое, не существующее имя поля данных. А что будет там с биндингом? Странно, но не так уж и критично.

Локализация

Честно говоря, не ожидал, что в 2013 году прочту следующие рекомендации по локализации конкретной строки в комопненте:
For the group headers, you can create a customized Group implicit DataTemplate that displays the information how you want it:
<DataTemplate DataType="{x:Type xcdg:Group}">
    <TextBlock Text="Voici mon DataTemplate français!" />
</DataTemplate>

You can start from the one availlable in the «themes\Common\Common.Resources.xaml» of the DataGrid for WPF installation folder (3rd resource from the top of the ResourceDictionary).

Вольный перевод:
Для текста предложения группировки в гриде используйте такой фрагмент кода:
непереводимый фрагмент кода
Затем можете начать смотреть шаблоны для переопределения здесь: «themes\Common\Common.Resources.xaml» по пути установки DataGrid Для WPF (3-й ресурс сверху)
Это я должен переопределить все шаблоны всех компонентов с текстовыми сообщениями? Ничего себе локализация в 21-м веке.

А вот эта ветка форума исключила Xceed из дальнейшего рассмотрения:
Unfortunately, it is not currently possible to change the language of the ShowPrintPreviewWindow or the ShowPrintPreviewPopup.

The print preview is not implemented using resource files (that preserves some language settings). Also, the printPreview is a control hosted inside a popup or a window (depending on if we are in an XBAP or not), and the control is private.

Краткий перевод:
К сожалению сейчас невозможно изменить язык сообщений этих окон, потому что этот текст не вынесен в ресурсные файлы, а компонент является приватным.
Браво, разработчики Xceed! Весь не англоязычный мир вам рукоплещит.

Вердикт

Красиво, быстро, многое удобно, но как в старом анекдоте «…есть один маленький нюанс».

ComponentOne


Впечатление положительное. Все сделано красиво и аккуратно. По разметке грид в базовых вещах практически не отличим от стандартного грида. Все биндинги работают, как и ожидается, колонки кастомизируются стилями или шаблонами, есть разные типы колонок с соответствующими редакторами. Но давайте пойдем по порядку.

Два грида вместо одного. Опять выбирать?

В поставке идет сразу два грида: C1DataGrid и C1FlexGrid. После того, как я увидел сей факт, мне сразу захотелось забыть про эти компоненты, но я себя пересилил и прочитал статью, объясняющую сей факт достаточно подробно и понятно, в отличие от Infragistics. Здесь функционал понятно разложен на два грида: первый с богатой возможностью кастомизации, фильтрацией, группировкой, мастер-деталь и т.д. А второй легкий, простой для быстрой работы с большими плоскими таблицами. Так что в данном случае это разделение можно даже назвать преимуществом.

Биндинг в колонки, кастомизация колонок

Здесь все прекрасно: задаем у колонки свойство Binding и радуемся гению разработчика. Только для того, чтобы иметь возможность редактировать значения в гриде надо явно задать двунаправленный биндинг. Так же прекрасно работает редактирование свойств связаных сущностей, однако по-умолчанию биндинг в текстбоксе редактирования срабатывает не при потере фокуса, а моментально. Неожиданно, но это мелочи и к тому же настраивается в выражении биндинга.

Кастомизация колонок на достаточно высоком уровне. Есть 9 стандартных типов колонок + TemplateColumn. У всех колонок можно задать стиль ячейки. У TemplateColumn, как это следует из названия, можно задать шаблон. При этом для задания стиля есть два свойства CellStyle и CellContentStyle. Если задавать стиль контента ячейки, то внутри стиля необходимо прописывать странные биндинги, например вот так пришлось писать триггер для смены цвета фона ячейки в зависимости от статуса заказа:
<Style x:Key="SateCellStyle" TargetType="TextBlock">
    <Setter Property="Background" Value="Transparent"/>
    <Style.Triggers>
        <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=c1:DataGridRowPresenter}, Path=Row.DataItem.State.Code}" Value="new">
            <Setter Property="Background" Value="Wheat"/>
        </DataTrigger>
        <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=c1:DataGridRowPresenter}, Path=Row.DataItem.State.Code}" Value="confirmed">
            <Setter Property="Background" Value="LightGreen"/>
        </DataTrigger>
    </Style.Triggers>
</Style>

Ну да ладно, это неприятно, но не так критично.

Огорчило, что нельзя задать CellTemplateSelector. Конечно, в реальном приложении всегда можно перефразировать задачу, чтобы обойтись без селектора, но это настораживает.

Мастер-деталь

Здесь все прекрасно. Отлично нарисован стиль по-умолчанию, хорошо биндятся детали, разметка страдает минимально, не требуя дополнительных промежуточных тегов, как у DevExpress:
<c1:C1DataGrid.RowDetailsTemplate>
    <DataTemplate>
        <c1:C1DataGrid x:Name="gridOrderItems" IsReadOnly="False" AutoGenerateColumns="False"
                ItemsSource="{Binding Path=SalesOrderItems}">
            <c1:C1DataGrid.Columns>
                <c1:DataGridTextColumn Binding="{Binding Path=SalesOrderItemId, Mode=TwoWay}" Header="Item Id" />
                <c1:DataGridTextColumn Binding="{Binding Path=Product.Name, Mode=TwoWay}" Header="Product name" />
            </c1:C1DataGrid.Columns>
        </c1:C1DataGrid>
    </DataTemplate>
</c1:C1DataGrid.RowDetailsTemplate>

Во вложенном гриде все так же прекрасно редактируется, включая поля связанных объектов. Валидация по-умолчанию не навязчива и не раздражает при редактировании.

Древовидный грид

Задача простая: необходимо показать условно бесконечную иерархию объектов одного типа. Основные требования к интерфейсу: колонки объявляются и показываются один раз, в левой части каждой строки есть кнопка переключения видимости дочерних записей, уровни показываются с видимым отступом от левого края.

«Из коробки» такого функционала нет, но в примерах есть похожий:

Реализовано за счет видимости строк, которая переключается в триггере кнопки ToggleButton, которая лежит шаблоне заголовка строки. Пример интересен, работает достаточно шустро и симпатично. Но не работает сортировка, потому что данные должны подсовываться в определенном порядке, согласно иерархии наследования (грид продолжает работать с плоским списком). Это только видимость иерархии и для реальных проектов не подойдет. Вывод – красивая рекламная фишка, в прикладной системе малопригодная.

Объединение заголовков колонок.

Бизнес задача – сделать многоуровневый заголовок таблицы с объединением, вида:

Надо сказать, что рассматриваемый грид – лучший по функционалу объединения не только заголовков, но и ячеек с данными и заголовков строк. Как и ожидается, это все реализовано с помощью одного функционала.

У компонента C1FlexGrid есть свойство AllowMerging, так же можно задать отдельно для строк и/или колонок. Это свойство включает режим автоматического объединения смежных ячеек, если в них одинаковые значения. Работает классно, но есть подозрение, что на больших объемах будет подтормаживать (у нас были проблемы с этим в древнем WinForms гриде того же производителя).

У компонента C1DataGrid с этим немного сложнее, но терпимо. Такого простого свойства нет. Но можно перехватить событие MergingCells, в обработчике которого можно реализовать свою логику объединения ячеек. Звучит страшно, но в примерах есть два статических класса с расширяющими методами для ячеек грида, которые сводят эту логику к минимуму. В итоге, если не вдаваться во внутренности этих хелперов, код обработчика для объединения заголовков у меня выглядел так:
        public ComponentOneComparision()
        {
            InitializeComponent();

            _headerColumnRows = new[] { TopColumnHeaderRow, BottomColumnHeaderRow };
        }

        private DataGridRow[] _headerColumnRows;

        private void dataGrid_MergingCells(object sender, C1.WPF.DataGrid.DataGridMergingCellsEventArgs e)
        {
            // строки и колонки без заголовков
            var nonHeadersViewportCols = dataGrid.Viewport.Columns.ToArray();
            var nonHeadersViewportRows = dataGrid.Viewport.Rows.Where(r => !_headerColumnRows.Contains(r)).ToArray();

            // объединяем заголовки колонок
            foreach (var range in MergingHelper.Merge(Orientation.Vertical, _headerColumnRows, nonHeadersViewportCols, true))
            {
                e.Merge(range);
            }
        }

При этом, заголовки колонок в xaml выглядели так:
<c1:DataGridTemplateColumn Header="[Статус, текст]" />
<c1:DataGridTemplateColumn Header="[Статус, шаблон]"/>
<c1:DataGridTextColumn Header="[Статус, стиль]"/>

Так же пришлось добавить две кастомные строки?
<c1:C1DataGrid.TopRows>
    <c1:DataGridColumnHeaderRow x:Name="TopColumnHeaderRow"/>
    <c1:DataGridColumnHeaderRow x:Name="BottomColumnHeaderRow"/>
</c1:C1DataGrid.TopRows>

И необходимо отключить стандартные заголовки колонок, выставив свойство грида:
<c1:C1DataGrid HeadersVisibility="Row"

В итоге, после запуска получаем грид, с заголовком, показанном на рисунке выше. С одной стороны кода получилось достаточно много: два вспомогательных класса, обработчик события, принудительное создание кастомных строк-заголвоков колонок, переставшую работать сортировку, фильтрацию и группировку, потому что мы отключили стандартные заголовки колонок. С другой стороны здесь используется унифицированный механизм объединения ячеек, который позволяет не только объединять заголовки столбцов или строк, но и красиво показывать отчеты, в которых будут объединяться данные в столбцах, например по статусу или названию компании.

Ленивая догрузка записей и поддержка WCF DataServices

К сожалению ленивая догрузка записей заявлена только для Silverlight, а для WPF даже в демонстрационном примере ее нет. Заявлена поддержка WCF Ria Services, неплохо описано в этой статье

На форуме есть открытый тикет на реализацию поддержки WCF DataServices, но пока там тихо. Можно ожидать, что этот функционал, если и будет реализован, то будет весьма добротным.

Копирование в буфер и экспорт в Excel

Это работает. Просто работает без плясок с бубном. Но только для плоских таблиц. Не экспортит объединенные заголовки колонок. Для мастер-деталь грида копируются в буфер строки, которые в фокусе (мастер или деталь), в Excel экспортятся только строки мастер грида. Для шаблонных колонок в буфер копируются пустые строки, в Excel экспортится результат метода ToString() у объекта данных строки.

Печать содержимого грида

У грида есть метод Print(string docName), который открывает диалог выбора принтера для печати всего грида. Так же имеется метод:
List<FrameworkElement> GetPageImages(…)

который возвращает разбитый на страницы грид. Затем полученную коллекцию можно упаковать в PrintDocument и напечатать как xps документ, показав его в предпросмотре.

Локализация

Здесь тоже все прекрасно. «Из коробки» есть локализация на 21 языке, включая необходимые для нашего проекта (русский и китайский).

Поддержка UI Automation

В последнее время я немного изучал вопрос автоматизации UI тестирования и не мог не пройтись инструментом Coded UI Test Builder по основным моментам. Был разочарован. Грид для инструмента тестирования видится как единый компонент без содержимого. Это печально, потому что если захотим тестировать, то придется расширять компоненты, встраивая поддержку UI Automation. Но не сказал бы что это блокирующий функционал.

Вердикт

Пациент скорее жив, чем мертв. Симпатичный, достаточно быстрый грид с хорошей поддержкой основных принципов WPF. Местами есть недоработки, вроде отсутствия CellTemplateSelector’ов или не реализованного UI Automation, но они не так критичны.

Telerik


У нас уже имелся положительный опыт работы с этими компонентами, поэтому здесь было интересно сравнить по конкретным фичам с другими. Надо отметить, что стиль по-умолчанию у грида меня всегда отпугивала от него. Но на вкус и цвет все фломастеры разные, а тему можно поменять в одну строку кода.

Биндинг в колонки, кастомизация колонок

Здесь все лучше, чем у предыдущих испытуемых. Биндинг работает как и ожидается, данные в ячейках хорошо редактируются. Валидация по-умолчанию есть, она не напрягает. Биндинг (и, конечно же, валидация) срабатывает в момент потери текстбоксом фокуса (как и в оригинальном WPF TextBox).

У колонки можно задать и стиль, и шаблон, селектор шаблона и даже селектор стиля. Все, как и должно быть. Биндинг в стиле ожидаемый. Вот так выглядит стиль с дататригером на цвет фона, в зависимости от статуса заказа (сравните его со стилем для грида ComponentOne):
<Style x:Key="SatetCellStyle" TargetType="telerik:GridViewCell">
    <Setter Property="Background" Value="Transparent"/>
    <Style.Triggers>
        <DataTrigger Binding="{Binding Path=State.Code}" Value="new">
            <Setter Property="Background" Value="Wheat"/>
        </DataTrigger>
        <DataTrigger Binding="{Binding Path=State.Code}" Value="confirmed">
            <Setter Property="Background" Value="LightGreen"/>
        </DataTrigger>
    </Style.Triggers>
</Style>


Мастер-деталь

Здесь все так же ожидаемо и хорошо, но необходимо дописать специальный тег. Объявление грида деталей выглядит вот так:
<telerik:RadGridView.ChildTableDefinitions>
    <telerik:GridViewTableDefinition />
</telerik:RadGridView.ChildTableDefinitions>
<telerik:RadGridView.HierarchyChildTemplate>
    <DataTemplate>
        <telerik:RadGridView ItemsSource="{Binding Path=SalesOrderItems}" AutoGenerateColumns="False">
            <telerik:RadGridView.Columns>
                <telerik:GridViewDataColumn DataMemberBinding="{Binding Path=SalesOrderItemId}" />
                <telerik:GridViewDataColumn DataMemberBinding="{Binding Path=Product.Name}" />
            </telerik:RadGridView.Columns>
        </telerik:RadGridView>
    </DataTemplate>
</telerik:RadGridView.HierarchyChildTemplate>

Чуть больше, чем у ComponentOne, но до многоуровневой структуры, как у DevExpress еще далеко.

В деталь гриде строки отлично редактируется, грид выглядит симпатично.

Древовидный грид

Изначально попытался реализовать это в самом гриде, меняя GridViewTableDefinition на TreeListViewTableDefinition. Интерфейс не изменился. Поначалу я расстроился и в своей табличке сравнения записал жирный минус, но я нашел, что есть такой компонент RadTreeListView, который вместе с RadGridView имеют общего предка GridViewDataControl, и работает именно так, как ожидает пользователь.

Вот так задается разметка:
<telerik:RadTreeListView ItemsSource="{Binding Path=Departments}"
                         AutoGenerateColumns="False" IsReadOnly="True"
                         SelectionMode="Extended"
                         ClipboardCopyMode="All">
    <telerik:RadTreeListView.Columns>
        <telerik:GridViewDataColumn DataMemberBinding="{Binding Path=DepartmentId}" Header="Идентификатор" />
        <telerik:GridViewDataColumn DataMemberBinding="{Binding Path=Name}" Header="Наименование" />
        <telerik:GridViewDataColumn DataMemberBinding="{Binding Path=Head.Name}" Header="Вышестоящее"/>
    </telerik:RadTreeListView.Columns>
    <telerik:RadTreeListView.ChildTableDefinitions>
        <telerik:TreeListViewTableDefinition ItemsSource="{Binding Children}"/>
    </telerik:RadTreeListView.ChildTableDefinitions>
</telerik:RadTreeListView>

При запуске получается вот такой грид:


Объединение заголовков колонок. Многоуровневые заголовки колонок.

Здесь тоже все неплохо. Объединенные заголовки колонок задаются в разметке таким образом:
<telerik:RadGridView.ColumnGroups>
    <telerik:GridViewColumnGroup Header="Состояние" Name="StateGroup"/>
</telerik:RadGridView.ColumnGroups>
<telerik:RadGridView.Columns>
<telerik:GridViewDataColumn Header="Текст" ColumnGroupName="StateGroup"/>
<telerik:GridViewDataColumn Header="Селектор" ColumnGroupName="StateGroup"/>
<telerik:GridViewDataColumn Header="Стиль" ColumnGroupName="StateGroup"/>
</telerik:RadGridView.Columns>

Выглядит вот так:

При этом, в отличие от ComponentOne, остается весь функционал заголовков колонок:

Но у Telerik нельзя объединять ячейки данных, заголовки строк и создавать действительно многоуровневые заголовки колонок (более 2-х уровней). Печально, но терпимо.

Ленивая догрузка записей и поддержка WCF DataServices

В библиотеках Telerik есть поддержка ленивой догрузки записей при прокрутке в гриде. Если в источник данных грида положить VirtualQueryableCollectionView, и реализовать обработчик события ItemsLoading, то при прокрутке грид сам будет запрашивать загрузку следующей порции данных. Так же в библиотеке есть расширяющие методы для System.Linq.IQueryable, которые позволяют сформировать такой запрос:
VirtualQueryableCollectionView EntityList;
…
var query = (DataServiceQuery) _dataContext.SalesOrders
    .IncludeTotalCount()
    .Sort(EntityList.SortDescriptors)
    .Where(EntityList.FilterDescriptors)
    .Skip(start)
    .Take(count);


В итоге в несколько движений получаем ленивый грид с серверной фильтрацией и сортировкой.

Копирование в буфер и экспорт в Excel

Работает аналогично гриду ComponentOne за тем исключением, что в результирующем Excel добавляются фильтры в заголовки колонок. Объединенные заголовки колонок отлично видятся в Excel. Мастер-деталь грид экспортит только содержимое мастер таблицы, древовидный грид экспортит плоскую таблицу без иерархии. В целом терпимо, но для более сложных сценариев надо кастомизировать.

Кастомизируется копирование в буфер, через перехват события CopyingCellClipboardContent, в обработчике которого сделать все, что душе угодно.

Печать содержимого грида

В гриде для WPF этого функционала нет и не предвидится. Официальный форум предлагает использовать для этого Telerik Reporting.

Локализация

Есть локализация на 5 языков, но нет русского и китайского. На сайте есть перечень ресурсов, необходимых для локализации каждого из контролов. Так что это решается наймом переводчика.

Поддержка UI Automation

Здесь лучше, чем у ComponentOne – ячейки грида все-таки распознаются как ячейки грида, но значения RowIndex и ColumnIndex у них равны нулю. Вообще в этом аспекте грид от Xceed оказался лучшим.

Вердикт

Есть недочеты, но не очень критические. Да, рывками движется скроллер при большом количестве записей (у Xceed это поведение лучше всех). Да, невозможно напечатать содержимое грида вызовом одного метода и надо править экспорт в Excel многоуровневых списков. Но в целом грид не плох.

Выводы


В процессе написания этой статьи я наткнулся на интересный обзор, датированный августом 2008 года. Автор приходит к неутешительному выводу, что, не смотря на то, что все представленные 5 компаний являются лидерами, ни одна из них на то время не предоставляла достаточно качественного грида.

Прошло 4.5 года. Можно сказать, что все, кроме Infragistics шагнули далеко вперед и выбор по-прежнему сделать трудно.

  • DevExpress выпустили более менее стабильный грид, но не смогли избавиться от наследия WinForms.
  • Xceed сделали быстрый и красивый грид, отличная поддержка виртуализации и WCF DataServices. Но биндинг в колонки, словарь колонок с ключом по названиям и принципиальное отсутствие возможности локализации сильно разочаровали.
  • ComponentOne сделали хороший быстрый и функциональный грид.
  • Telerik сумели избавиться от наследия WinForms (даже не знаю чего это стоило разработчикам – наверняка не один раз переписали все «с нуля»).

Если подводить итог сравнения и давтаь рекомендации по выбору, то я бы сказал так. Если обязательно нужна локализация на несколько языков, необходимо объединение ячеек данных и при этом не критична поддержка IQueryable и в явных бизнес-требованиях нет реализации иерархического грида, то ComponentOne будет хорошим выбором. В противном случае, я бы остановился на гриде от Telerik.

Если говорить про требования нашего проекта, то мы разрываемся между двумя последними. Нам необходима поддержка русского и китайских языков (в перспективе и других), но в то же время нам нужен иерархический грид и поддержка WCF DataServices. Мы пока не пришли к конечному выбору и взвешиваем, что же будет дороже: перевести все ресурсы на два языка (не влияет на работоспособность системы) или же собственная реализация иерархического грида и поддержки WCF DataServices (потенциально могут быть ошибки при реализации).
Tags:
Hubs:
+33
Comments 33
Comments Comments 33

Articles