WPF DataGrid с форматирани текстови клетки

В настройка с AutoGenerateColmns=True се опитвам да намеря начин, който да ми позволи да показвам многоцветен текст в една клетка на C# DataGrid в WPF. Както решение с персонализиран шаблон на клетка, така и с HTML интерпретатор в клетката би било подходящо; поради технически причини бих предпочел първия вариант, но прекарах много време в опити да накарам някое от тези да работи, но без резултат.

Допълнителен проблем е, че имам работа с няколко таблици тук, които трябва да покажа, използвайки същия потребителски контрол. Само една от таблиците ще изисква форматиране на текст. Разбрах как да променя динамично естеството на таблицата въз основа на критерий по мой избор, но не знам как да попълня тези модифицирани клетки въз основа на действителния текст, който влиза там. Самият текст съдържа токен ("|||"—три тръби), при който да се раздели низът; единият трябва да бъде показан в синьо, другият в червено.

Нека ви покажа моя код. Ще започна с извадка от App.xaml, за да е ясно как съм настроил своя DataGrid стилово.

My App.xaml

Както ще видите, трябва да накарам DataGrid да генерира автоматично колоните, тъй като работя върху база данни, чиято схема не ми е известна по време на компилиране. Тъй като заглавките на таблицата може да съдържат долни черти, създадох персонализиран шаблон, който не третира долните черти по обичайния начин, т.е. като комбинации от кратки клавиши с Alt.

<Style x:Key="DataGridStyle" TargetType="DataGrid">
  <Setter Property="AutoGenerateColumns" Value="True" />
  <Setter Property="CanUserAddRows" Value="False" />
  <Setter Property="ColumnHeaderStyle">
    <Setter.Value>
      <Style TargetType="DataGridColumnHeader">
        <Setter Property="ContentTemplate">
          <Setter.Value>
            <DataTemplate>
              <TextBlock Text="{TemplateBinding Content}" 
                         HorizontalAlignment="Center"/>
            </DataTemplate>
          </Setter.Value>
        </Setter>
      </Style>
    </Setter.Value>
  </Setter>
  <Setter Property="CanUserSortColumns" Value="True" />
</Style>

Самият DataGrid

Това има някои модификации, една от които е, че използвам персонализиран RowDetailsTemplate въз основа на друг потребителски контрол. Това не е от значение (надявам се), но го оставям тук за пълнота (и в случай, че има някои странични ефекти, които пропускам). ExpView е пространството от имена, в което работя; Самият „Exp“ е това, което се нарича софтуерът (поради което предшественикът на DataGrid се нарича „ExpView:ExpResultsTable“).

Виждате, че съм абониран за две събития тук. Опитвах се колкото е възможно повече да запазя кода си празен (както се препоръчва в документите на MSDN за чиста имплементация на MVVM), но страхувам се, че човек може да направи толкова много само с XAML... Чувствайте се свободни да ме коригирате, все пак. Тези събития са това, което използвам, за да определя дали DataGrid ще бъде за специалната маса или за нормалните.

<DataGrid Name="DataGrid_Calibs"
          Style="{StaticResource DataGridStyle}"
          SelectedItem="{Binding Path=SelectedCalib,
                                 RelativeSource={RelativeSource AncestorType=ExpView:ExpResultsTable}}"
          AutoGeneratingColumn="DataGrid_Calibs_AutoGeneratingColumn"
          LoadingRow="DataGrid_Calibs_LoadingRow">
  <DataGrid.RowDetailsTemplate>
    <DataTemplate>
      <ExpView:CalibrationDetails/>
    </DataTemplate>
  </DataGrid.RowDetailsTemplate>
</DataGrid>

В началото на този XAML файл имам и следната декларация за ресурс, която влиза в клетките, в случай че трябва да покажа „специалната“ таблица.

<UserControl.Resources>
  <ResourceDictionary>
    <DataTemplate x:Key="DifferenceDataTemplate">
      <StackPanel Orientation="Vertical">
        <TextBlock x:Name="FieldValue_Old" Foreground="Blue"/>
        <TextBlock x:Name="FieldValue_New" Foreground="Red"/>
      </StackPanel>
    </DataTemplate>
  </ResourceDictionary>
</UserControl.Resources>

Планът е таблицата да се попълни с нормално обвързване на данни, ако трябва да показва нормални данни. (Аз не попълвам DataGrid с RowViews; вместо това обвързвам DataGrid към обекти, които правилно показват публични свойства. Ще видите ефекта от това в моя код за обработка на събития по-долу.) Ако данните са „специални“, Искам да заменя клетките с StackPanel, дефинирани в ресурсите по-горе, и да задам съдържанието на двете TextBlocks (или Labels, ако се окаже, че работят по-добре) по отношение на първата и втората част от низа, които трябва да влязат в клетка, разделена на "|||".

Обработчиците на събития в задния код

Хубавото на WPF манипулатора на събития за автоматично генериране на колони на DataGrid е, че той върши нормалната си работа, освен ако изрично не презапишете колоната. Ето защо следният код работи добре при замяната на клетките на DataGrid с гореспоменатите StackPanel само в случай, че открия „специална таблица“.

private void DataGrid_Calibs_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e) {
  ParameterDetails parameterDetailsControl = ((ScrollViewer)((DockPanel)((StackPanel)((DataGrid)sender).Parent).Parent).Parent).Parent as ParameterDetails;
  ExpUserInterface parent = Window.GetWindow((DependencyObject)parameterDetailsControl) as ExpUserInterface;
  ExpResultsTable resultsTableControl = parent.ResultsTable.Content as ExpResultsTable;
  string selector = resultsTableControl.SelectedTabItem.Header.ToString();

  if (selector.Equals("special")) {
    DifferenceDataTemplateColumn col = new DifferenceDataTemplateColumn();
    col.Header = e.PropertyName;
    col.CellValue = e.PropertyName;
    col.CellTemplate = (DataTemplate)FindResource("DifferenceDataTemplate");
    col.CellTemplate.DataType = typeof(MyDatabaseObject);
    col.IsReadOnly = true;
    e.Column = col;
  }
}

private void DataGrid_Calibs_LoadingRow(object sender, DataGridRowEventArgs e) {
  MyDatabaseObject calib = e.Row.DataContext as MyDatabaseObject;
  // anything I can do about the problem in here?
}

Вторият манипулатор на събития е тук най-вече, за да ви покаже, че наистина се свързвам с MyDatabaseObject вместо с RowView.

С готовност признавам, че първите четири реда са грозни, но поне засега работят. Всякакви предложения обаче са добре дошли.

Останалото също работи добре: „нормалните“ таблици се попълват, както е планирано, а „специалната“ таблица е форматирана с моя персонализиран StackPanels във всяка клетка. Не успявам обаче да попълня тези клетки.

Въпросите

Ето върху какво размишлявам от дни и което не мога да разбера, задавайки запитвания към обичайните заподозрени (Stackoverflow, MSDN и т.н.):

  • Как мога да получа достъп до съдържанието на клетката? Събитието LoadingRow е мястото, където трябва да получа достъп до низа, да го разделя и да задам FieldValue_Old.Text и FieldValue_New.Text. Мога да осъществя достъп до обекта чрез calib (вижте по-горе)—дебъгерът показва, че съм точно там—но за живота си не мога да разбера как да осъществя достъп до TextBlocks на една („специална”) клетка, нека сам как да направя това за всички колони на реда под ръка.
  • Мога ли да разреша това с помощта на CellTemplateSelector? как?
  • Използвам ли правилния манипулатор на събития? Има ли събитие, което да предпочета пред тези?
  • Има ли друг начин да се постигне този резултат? Например, мога ли да убедя DataGrid да интерпретира HTML в своите клетки? (Не е нужно да се тревожа за инжектиране на код тук, тъй като бих генерирал HTML код само в момента на попълване на DataGrid.)

Всяка помощ е високо ценена и ще ме предпази от бързо приближаване до плешивост, докато си късам косата заради това. Благодаря ти!

Актуализация

Забравих да добавя кода за персонализирания DataGridTemplateColumn, за който имах предвид в гореспоменатия манипулатор на събития AutoGeneratingColumn.

public class DifferenceDataTemplateColumn : DataGridTemplateColumn {
    public string CellValue { get; set; }

    protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem) {
      ContentPresenter presenter = (ContentPresenter)base.GenerateElement(cell, dataItem);
      BindingOperations.SetBinding(presenter, ContentPresenter.ContentProperty, new Binding(this.CellValue));
      return presenter;
    }
  }

person Informagic    schedule 14.07.2016    source източник


Отговори (1)


Оказва се, че съм бил наистина близо до решението; може да се постигне без да се прибягва до DataGrid_Calibs_LoadingRow или TemplateSelector. Все още се нуждаем от манипулатора на събития DataGrid_Calibs_AutoGeneratingColumn, но само за да приложим правилно персонализирания шаблон на клетка с двата TextBlocks, и все още се нуждаем от персонализирания DataGridTemplateColumn.

Новият потребителски контролен XAML код

Номерът беше да се разширят дефинициите на ресурсите на потребителския контрол.

<UserControl.Resources>
  <ResourceDictionary>
    <DataTemplate x:Key="DifferenceDataTemplate">
      <StackPanel Orientation="Vertical">
        <TextBlock x:Name="FieldValue_Old" Foreground="Blue" Text="{Binding Converter={StaticResource fieldValueConverter}, ConverterParameter={StaticResource True}}"/>
        <TextBlock x:Name="FieldValue_New" Foreground="Red" Text="{Binding Converter={StaticResource fieldValueConverter}, ConverterParameter={StaticResource False}}"/>
      </StackPanel>
    </DataTemplate>
  </ResourceDictionary>
</UserControl.Resources>

Допълнения към App.xaml

Дефинирах конвертора по начин, който позволява да се използва и за двете TextBlocks, но за да го направя, трябва да предам bool. Тъй като XAML по подразбиране предава ConverterParameters като string, дефинирах два нови ресурса в моя App.xaml в допълнение към fieldValueConverter.

<Application ...
             xmlns:s="clr-namespace:System;assembly=mscorlib"
             xmlns:MyNamespace="clr-namespace:MyNamespace"
             ... >
  <Application.Resources>
    <MyNamespace:FieldValueConverter x:Key="fieldValueConverter" />
    <s:Boolean x:Key="True">True</s:Boolean>
    <s:Boolean x:Key="False">False</s:Boolean>
    ...
  </Application.Resources>
</Application>

Допълнения към App.xaml.cs

Оставащото, което трябва да направите, е да дефинирате действителния преобразувател на стойност в задния код на App.xaml.

[ValueConversion(typeof(string), typeof(string))]
public class FieldValueConverter : IValueConverter {
  public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
    bool isOld = (bool)parameter;
    string[] fieldValues = value.ToString().Split(new string[] {"|||"}, System.StringSplitOptions.None);

    string fieldValue = "";
    if (isOld)
      fieldValue = fieldValues[0];
    else if (!isOld && fieldValues.Length == 2)
      fieldValue = fieldValues[1];
    return fieldValue;
  }

  public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
    throw new NotImplementedException("Back conversion of comparison fields is not supported.");
  }
}

Единствената причина за проверката fieldValues.Length == 2 е, че за полета, които не съдържат низа "|||", не искам второто поле да показва нищо. Освен това, тъй като полетата, които не съдържат разлика, са празни по подразбиране, връщам празния низ по подразбиране.

И това е всичко, което липсваше! Сега работи като чар.

Бонус въпрос

Не виждам защо е необходим персонализираният DataGridTemplateColumn. Той предоставя друго свойство за достъп до това, което наричам CellValue по-горе, тъй като моето обвързване на данни иначе просто би извикало метода ToString() на обвързаните обекти, т.е. клетките съдържат името на обекта, а не стойността на полето. Но защо?

Благодарности

Много благодаря за отговорите, публикувани на това и този въпрос тук в StackOverflow.

person Informagic    schedule 26.07.2016