Однажды мне пришлось работать с классом, упрощённое описание которого выглядит так:
public class Test
{
public string TestText
{
get
{
if (TestDocument == null) return "Документ не определён";
return new TextRange(TestDocument.ContentStart, TestDocument.ContentEnd).Text;
}
}
public FlowDocument TestDocument { get; set; }
}
Нужно было представить ObservableCollection<Test> таким образом, чтобы в одной части окна в ListBox отображалась некая информация о документах (в нашем случае — это TestText), а в другой части окна в RichTextBox представлялась возможность редактирования документа текущего экземпляра Test.
Упрощённое визуальное представление этой задачи можно описать так:
<DockPanel>
<ToolBar DockPanel.Dock="Top" Name="toolBar1" Width="Auto">
<Button Name="Add" Content="Добавить" Click="Add_Click"/>
<Button Name="Delete" Content="Удалить" Click="Delete_Click"/>
</ToolBar>
<Grid DockPanel.Dock="Top" Height="Auto">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ListBox Name="listTests" Grid.Column="0" ItemsSource="{Binding}" IsSynchronizedWithCurrentItem="True">
<ListBox.ItemTemplate>
<DataTemplate DataType="local:Test">
<TextBlock Text="{Binding TestText}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<GridSplitter Width="5" Grid.Column="1" HorizontalAlignment="Left"/>
<!--
<RichTextBox Name="testBox" Grid.Column="1" Margin="5,0,0,0" Document="{Binding TestDocument}"/>
-->
</Grid>
</DockPanel>
Как видим, для выполнения этой задачи необходимо решить две проблемы:
1. Связать RichTextBox.Document с TestDocument текущего экземпляра Test приблизительно так, как написано в комментарии. Но такое описание недопустимо потому, что RichTextBox.Document не является DependencyProperty.
2. Синхронизировать вводимые в RichTextBox данные с информацией о документе в ListBox. Эта проблема возникает потому, что при редактировании документа в RichTextBox меняется только содержимое документа. Экземпляр документа при этом остаётся прежним.
Решение первой проблемы заключается в создании наследника от RichTextBox, который бы сделал свойство Document свойством зависимости:
public class DocumentBox : RichTextBox
{
protected override void OnTextChanged(TextChangedEventArgs e)
{
if (e.Changes.Count > 0) changedIndicator = true;
base.OnTextChanged(e);
}
public bool ResetChangedIndicator()
{
var retValue = changedIndicator;
changedIndicator = false;
return retValue;
}
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
if (e.Property == DocumentProperty) base.Document = Document;
base.OnPropertyChanged(e);
}
public new FlowDocument Document
{
get
{
if ((FlowDocument)GetValue(DocumentProperty) == null) return new FlowDocument();
return (FlowDocument)GetValue(DocumentProperty);
}
set { SetValue(DocumentProperty, value); }
}
public static readonly DependencyProperty DocumentProperty = DependencyProperty.Register("Document", typeof(FlowDocument), typeof(DocumentBox));
bool changedIndicator;
}
Теперь вы можете вместо RichTextBox использовать DocumentBox. Для этого, вместо закомментированной строчки в XAML нужно написать:
<local:DocumentBox x:Name="testBox" Grid.Column="1" Margin="5,0,0,0" Document="{Binding TestDocument}"/>
Чтобы опробовать это решение, используйте в классе Window1 такой отделённый код:
public Window1()
{
InitializeComponent();
DataContext = tests;
}
private void Add_Click(object sender, RoutedEventArgs e)
{
Test t = new Test();
t.TestDocument=new FlowDocument();
t.TestDocument.FontFamily = new FontFamilyConverter().ConvertFromString("Times New Roman") as FontFamily;
t.TestDocument.FontSize = 18.7;
t.TestDocument.Blocks.Add(new Paragraph(new Run("Содержимое " + (tests.Count+1) + "-го документа")));
tests.Add(t);
listTests.SelectedItem = t;
}
private void Delete_Click(object sender, RoutedEventArgs e)
{
tests.Remove((Test)listTests.Items.CurrentItem);
}
System.Collections.ObjectModel.ObservableCollection<Test> tests = new System.Collections.ObjectModel.ObservableCollection<Test>();
Попробуйте добавлять и удалять записи, изменять содержимое документа. Как видите, удалось "прибиндить" RichTextBox.Document (точнее DocumentBox.Document) к FlowDocument. Первая проблема решена.
Вторая проблема выражается в том, что ListBox не реагирует на редактирование документов, а хотелось бы.
Решить эту проблему можно двумя способами:
1. Принудительно обновлять список в событии TextChanged DocumentBox-а:
private void DocumentBox_TextChanged(object sender, TextChangedEventArgs e)
{
if(e.Changes.Count > 0) listTests.Items.Refresh();
}
Этот способ даёт красивый эффект, но будет сильно тормозить на больших FlowDocument-ах.
2. Принудительно обновлять список в событии потери клавиатурного фокуса DocumentBox-а:
private void DocumentBox_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
if (testBox.ResetChangedIndicator()) listTests.Items.Refresh();
}
Не забудьте обявить соответствующие события в XAML:
<local:DocumentBox x:Name="testBox" Grid.Column="1" Margin="5,0,0,0" Document="{Binding TestDocument}" TextChanged="DocumentBox_TextChanged"/>
или
<local:DocumentBox x:Name="testBox" Grid.Column="1" Margin="5,0,0,0" Document="{Binding TestDocument}" LostKeyboardFocus="DocumentBox_LostKeyboardFocus"/>