如何强制刷新FlowDocument在DataContext设置后的内容?

3
我正在尝试使用具有绑定的FlowDocument,以便有一个能够使用数据模型中的实际数据填充的单独模板。然后我将其转换为图像并打印或保存到硬盘上。 为了将FlowDocument的Runs与数据模型绑定,我使用了这篇文章中的代码:https://msdn.microsoft.com/en-us/magazine/dd569761.aspx FlowDocument模板如下:
<FlowDocument xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
          xmlns:p="clr-namespace:Labels;assembly=DataModel.Impl"
PageWidth="200" MinPageWidth="200" PageHeight="200" MinPageHeight="200">

<Section>
  <Paragraph>
    <p:BindableRun BoundText="{Binding Path=Text}"/>
  </Paragraph>
</Section>

</FlowDocument>

BindableRun的代码:

public class BindableRun : Run
  {
    public static readonly DependencyProperty BoundTextProperty = DependencyProperty.Register("BoundText", typeof(string), typeof(BindableRun),
      new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure, OnBoundTextChanged, CoerceText));

    public BindableRun()
    {
      FlowDocumentHelpers.FixupDataContext(this);
    }

    private static void OnBoundTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
      ((Run)d).Text = (string)e.NewValue;
    }

    private static object CoerceText(DependencyObject d, object value)
    {
      return value;
    }

    public String BoundText
    {
      get { return (string)GetValue(BoundTextProperty); }
      set { SetValue(BoundTextProperty, value); }
    }
  }

然后我加载模板并在其中设置DataContext:
private class DataClass
{
  public string Text { get; set; }
}

private static FlowDocument LoadFlowDocument(string path)
{
  using (var xamlFile = new FileStream(path, FileMode.Open, FileAccess.Read))
  {
    return XamlReader.Load(xamlFile) as FlowDocument;
  }
}

    private static void FlowDoc2Image(FlowDocument document, DataClass dataContext, Stream imageStream)
{
  var flowDocumentScrollViewer = new FlowDocumentScrollViewer
  {
    VerticalScrollBarVisibility = ScrollBarVisibility.Hidden,
    HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden,
    DataContext = dataContext
  };

  flowDocumentScrollViewer.Document = document;

  flowDocumentScrollViewer.Measure(new Size(999999999, 999999999));

  //1st pass
  flowDocumentScrollViewer.Arrange(new Rect(0, 0, flowDocumentScrollViewer.ActualWidth, flowDocumentScrollViewer.ActualHeight));
  //2nd pass. It's not code duplication! Do not remove!
  flowDocumentScrollViewer.Arrange(new Rect(0, 0, flowDocumentScrollViewer.ActualWidth, flowDocumentScrollViewer.ActualHeight));

  var bitmapRenderer =
    new RenderTargetBitmap((int)flowDocumentScrollViewer.ActualWidth, (int)flowDocumentScrollViewer.ActualHeight, 96, 96, PixelFormats.Pbgra32);

  bitmapRenderer.Render(flowDocumentScrollViewer);

  var pngEncoder = new PngBitmapEncoder { Interlace = PngInterlaceOption.On };
  pngEncoder.Frames.Add(BitmapFrame.Create(bitmapRenderer));

  pngEncoder.Save(imageStream);
}

public void Test()
{
  var doc = LoadFlowDocument("C:\\Experiments\\DocWithBinding.xaml");

  var context = new DataClass {Text = "SomeText"};
  doc.DataContext = context;

  using (var imageStream = new FileStream("C:\\Experiments\\image.png", FileMode.OpenOrCreate, FileAccess.Write))
  {
    FlowDoc2Image(doc, context, imageStream);
  }
}

但是什么也没发生。我试图在BindableRun中设置断点,以便在其值更改时触发。但我从未到达那里。更改DataContext不会影响文档。


首先,在绑定文本属性的Getter和Setter上设置断点,然后告诉我们它们是否被调用。 - JWP
@John Peters 是的,当我写下“我尝试在 BindableRun 中设置断点以在更改其值时进行调试”时,这就是我的意思。因此,我在 Getter 和 Setter 上设置了断点,但没有反应。 - ded.diman
当在XAML中访问属性时,WPF会绕过依赖属性的CLR包装器的getter和setter。因此,任何可能的断点都不会被触发。有关详细信息,请参见XAML加载和依赖属性 - Clemens
@Clemens Getter和Setter属性的CLR包装器在任何时候都可以被破坏,此外,在OnBoundTextChange方法中还可以看到另一个属性。 - JWP
@Clemens 你说的有一定道理,但并不完全正确。有时候WPF不会调用Getter Setter,但有时候它会,然而在这种情况下它总是会调用OnTextPropertyChanged,除非绑定不正确,这是问题的根本原因。 - JWP
@Clemens 显然,以上任何建议都与解决方案的根本原因无关。当人们甚至没有走上正确的道路时,他们想要进行完美的争吵,这让我感到烦恼。我已在下面的答案中为op发布了正确的解决方案路径。 - JWP
1个回答

2
现在不再需要BindableRun类。从Run.Text的备注部分可以看到:

从 .NET Framework 4 开始,Run对象的Text属性是一个依赖属性,这意味着您可以将Text属性绑定到数据源。

因此,您的FlowDocument文件可能如下所示:

<FlowDocument xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    PageWidth="200" MinPageWidth="200" PageHeight="200" MinPageHeight="200">
    <Section>
        <Paragraph>
            <Run Text="{Binding Text}"/>
        </Paragraph>
    </Section>
</FlowDocument>

我按照你的问题所示加载了它,并将DataClass实例分配给其DataContext,成功地在RichTextBox中显示出来:

<Grid>
    <RichTextBox x:Name="rtb"/>
</Grid>

代码后台:

private class DataClass
{
    public string Text { get; set; }
}

public MainWindow()
{
    InitializeComponent();

    var doc = LoadFlowDocument("DocWithBinding.xaml");
    doc.DataContext = new DataClass { Text = "Hello, World." };
    rtb.Document = doc;
}

private static FlowDocument LoadFlowDocument(string path)
{
    using (var xamlFile = new FileStream(path, FileMode.Open, FileAccess.Read))
    {
        return XamlReader.Load(xamlFile) as FlowDocument;
    }
}

编辑 虽然您可以成功地将FlowDocument放入FlowDocumentScrollViewer中,但同步渲染此查看器到RenderTargetBitmap似乎无法创建所需的输出。感觉绑定尚未建立,因为文档中的硬编码文本将同步呈现。

我尝试了一些方法,但似乎无法避免在呈现位图之前添加短暂的延迟。我通过使FlowDoc2Image方法异步并调用await Task.Delay(100)来实现这一点。虽然这是一个hack,但它可以创建PNG。

private async Task FlowDoc2Image(
    FlowDocument document, DataClass dataContext, Stream imageStream)
{
    var flowDocumentScrollViewer = new FlowDocumentScrollViewer
    {
        VerticalScrollBarVisibility = ScrollBarVisibility.Hidden,
        HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden,
        Document = document,
        DataContext = dataContext,
    };

    flowDocumentScrollViewer.Measure(
        new Size(double.PositiveInfinity, double.PositiveInfinity));
    flowDocumentScrollViewer.Arrange(new Rect(flowDocumentScrollViewer.DesiredSize));

    await Task.Delay(100);

    var renderTargetBitmap = new RenderTargetBitmap(
        (int)flowDocumentScrollViewer.DesiredSize.Width,
        (int)flowDocumentScrollViewer.DesiredSize.Height,
        96, 96, PixelFormats.Default);

    renderTargetBitmap.Render(flowDocumentScrollViewer);

    var pngEncoder = new PngBitmapEncoder { Interlace = PngInterlaceOption.On };
    pngEncoder.Frames.Add(BitmapFrame.Create(renderTargetBitmap));
    pngEncoder.Save(imageStream);
}

好的,这是个好消息!我移除了BindableRun并直接将我的数据模型与Run绑定。但结果仍然相同。在设置DataContext后,我在调试器中观察到Run.Text的状态,它等于“”。当我将FlowDocument保存到文件时,我得到了以下内容:<FlowDocument PageWidth="200" MinPageWidth="200" PageHeight="200" MinPageHeight="200" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <Section> <Paragraph /> </Section> </FlowDocument> - ded.diman
在将FlowDocument的DataContext设置后,我不知道您实际上要做什么。但是,将其放入RichTextBox中可以使其正确显示。 - Clemens
好的,为了实验,我在代码中创建了一个RichTextBox实例,并将我的FlowDocument实例和数据模型实例作为DataContext传递给它。看起来RichTextBox会以某种方式触发FlowDocument从数据模型获取实际数据。完美!但不幸的是,它失败并抛出异常:“BindingExpression路径错误:在'对象'上找不到'Text'属性”。 那么我是否需要在FlowDocument xaml中为我的DataClass创建DataTemplate?如何应用它呢?在WPF中,我会使用ContentControl和其中的ContentTemplate属性。但是在FlowDocument中呢? - ded.diman
非常有趣!所以绑定是异步设置的,但没有任何公共API进行同步,是吗?好的,我会考虑一下。我已经几乎实现了一种扩展FlowDocument的方法,可以替代普通的绑定。虽然有点繁琐,但至少很直接,没有任何技巧。:)) 非常感谢您的调查!它让我有很多思考。 - ded.diman
我不确定这究竟是绑定问题还是查看器控件内部的一些异步文档加载机制。然而,在FlowDocumentScrollViewer中,我也找不到任何同步事件。 - Clemens
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接