使用MVVM双向绑定到AvalonEdit文档文本

我想在我的MVVM应用程序中包含一个AvalonEdit TextEditor控件。 我需要的第一件事是能够绑定到TextEditor.Text属性,以便我可以显示文本。 为此,我遵循了Make AvalonEdit MVVM兼容的示例。 现在,我已使用接受的答案作为模板实现了以下类

 public sealed class MvvmTextEditor : TextEditor, INotifyPropertyChanged { public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(MvvmTextEditor), new PropertyMetadata((obj, args) => { MvvmTextEditor target = (MvvmTextEditor)obj; target.Text = (string)args.NewValue; }) ); public new string Text { get { return base.Text; } set { base.Text = value; } } protected override void OnTextChanged(EventArgs e) { RaisePropertyChanged("Text"); base.OnTextChanged(e); } public event PropertyChangedEventHandler PropertyChanged; public void RaisePropertyChanged(string info) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(info)); } } 

XAML在哪里

  

首先,这不起作用。 绑定根本没有在Snoop中显示(不是红色,不是任何东西,实际上我甚至看不到Text依赖属性)。

我已经看到这个问题与我的完全相同AvalonEdit中的双向绑定不起作用但是接受的答案不起作用(至少对我而言)。 所以我的问题是:

如何使用上面的方法执行双向绑定以及我的MvvmTextEditor类的正确实现是什么?

谢谢你的时间。


注意:我的ViewModel中有Text属性,它实现了所需的INotifyPropertyChanged接口。

创建一个将附加TextChanged事件的Behavior类,并将挂钩绑定到ViewModel的依赖项属性。

AvalonTextBehavior.cs

 public sealed class AvalonEditBehaviour : Behavior { public static readonly DependencyProperty GiveMeTheTextProperty = DependencyProperty.Register("GiveMeTheText", typeof(string), typeof(AvalonEditBehaviour), new FrameworkPropertyMetadata(default(string), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, PropertyChangedCallback)); public string GiveMeTheText { get { return (string)GetValue(GiveMeTheTextProperty); } set { SetValue(GiveMeTheTextProperty, value); } } protected override void OnAttached() { base.OnAttached(); if (AssociatedObject != null) AssociatedObject.TextChanged += AssociatedObjectOnTextChanged; } protected override void OnDetaching() { base.OnDetaching(); if (AssociatedObject != null) AssociatedObject.TextChanged -= AssociatedObjectOnTextChanged; } private void AssociatedObjectOnTextChanged(object sender, EventArgs eventArgs) { var textEditor = sender as TextEditor; if (textEditor != null) { if (textEditor.Document != null) GiveMeTheText = textEditor.Document.Text; } } private static void PropertyChangedCallback( DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs) { var behavior = dependencyObject as AvalonEditBehaviour; if (behavior.AssociatedObject!= null) { var editor = behavior.AssociatedObject as TextEditor; if (editor.Document != null) { var caretOffset = editor.CaretOffset; editor.Document.Text = dependencyPropertyChangedEventArgs.NewValue.ToString(); editor.CaretOffset = caretOffset; } } } } 

View.xaml

       

其中i被定义为“xmlns:i =”clr-namespace:System.Windows.Interactivity; assembly = System.Windows.Interactivity“”

ViewModel.cs

  private string _test; public string Test { get { return _test; } set { _test = value; } } 

这应该给你Text并将其推回到ViewModel。

另一个不错的OOP方法是下载AvalonEdit的源代码(它是开源的),并创建一个inheritance自TextEditor类(AvalonEdit的主编辑器)的新类。

你想要做的是基本上覆盖Text属性并实现它的INotifyPropertyChanged版本,使用Text属性的依赖属性并在文本更改时引发OnPropertyChanged事件(这可以通过覆盖OnTextChanged()方法来完成。

这是一个适合我的快速代码(完全工作)示例:

 public class BindableTextEditor : TextEditor, INotifyPropertyChanged { ///  /// A bindable Text property ///  public new string Text { get { return base.Text; } set { base.Text = value; } } ///  /// The bindable text property dependency property ///  public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(BindableTextEditor), new PropertyMetadata((obj, args) => { var target = (BindableTextEditor)obj; target.Text = (string)args.NewValue; })); protected override void OnTextChanged(EventArgs e) { RaisePropertyChanged("Text"); base.OnTextChanged(e); } ///  /// Raises a property changed event ///  /// The name of the property that updates public void RaisePropertyChanged(string property) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(property)); } } public event PropertyChangedEventHandler PropertyChanged; } 

对于那些想要使用AvalonEdit进行MVVM实现的人来说,这是其中一种方法,首先我们有类

 ///  /// Class that inherits from the AvalonEdit TextEditor control to /// enable MVVM interaction. ///  public class CodeEditor : TextEditor, INotifyPropertyChanged { // Vars. private static bool canScroll = true; ///  /// Default constructor to set up event handlers. ///  public CodeEditor() { // Default options. FontSize = 12; FontFamily = new FontFamily("Consolas"); Options = new TextEditorOptions { IndentationSize = 3, ConvertTabsToSpaces = true }; } #region Text. ///  /// Dependancy property for the editor text property binding. ///  public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(CodeEditor), new PropertyMetadata((obj, args) => { CodeEditor target = (CodeEditor)obj; target.Text = (string)args.NewValue; })); ///  /// Provide access to the Text. ///  public new string Text { get { return base.Text; } set { base.Text = value; } } ///  /// Return the current text length. ///  public int Length { get { return base.Text.Length; } } ///  /// Override of OnTextChanged event. ///  protected override void OnTextChanged(EventArgs e) { RaisePropertyChanged("Length"); base.OnTextChanged(e); } ///  /// Event handler to update properties based upon the selection changed event. ///  void TextArea_SelectionChanged(object sender, EventArgs e) { this.SelectionStart = SelectionStart; this.SelectionLength = SelectionLength; } ///  /// Event that handles when the caret changes. ///  void TextArea_CaretPositionChanged(object sender, EventArgs e) { try { canScroll = false; this.TextLocation = TextLocation; } finally { canScroll = true; } } #endregion // Text. #region Caret Offset. ///  /// DependencyProperty for the TextEditorCaretOffset binding. ///  public static DependencyProperty CaretOffsetProperty = DependencyProperty.Register("CaretOffset", typeof(int), typeof(CodeEditor), new PropertyMetadata((obj, args) => { CodeEditor target = (CodeEditor)obj; if (target.CaretOffset != (int)args.NewValue) target.CaretOffset = (int)args.NewValue; })); ///  /// Access to the SelectionStart property. ///  public new int CaretOffset { get { return base.CaretOffset; } set { SetValue(CaretOffsetProperty, value); } } #endregion // Caret Offset. #region Selection. ///  /// DependencyProperty for the TextLocation. Setting this value /// will scroll the TextEditor to the desired TextLocation. ///  public static readonly DependencyProperty TextLocationProperty = DependencyProperty.Register("TextLocation", typeof(TextLocation), typeof(CodeEditor), new PropertyMetadata((obj, args) => { CodeEditor target = (CodeEditor)obj; TextLocation loc = (TextLocation)args.NewValue; if (canScroll) target.ScrollTo(loc.Line, loc.Column); })); ///  /// Get or set the TextLocation. Setting will scroll to that location. ///  public TextLocation TextLocation { get { return base.Document.GetLocation(SelectionStart); } set { SetValue(TextLocationProperty, value); } } ///  /// DependencyProperty for the TextEditor SelectionLength property. ///  public static readonly DependencyProperty SelectionLengthProperty = DependencyProperty.Register("SelectionLength", typeof(int), typeof(CodeEditor), new PropertyMetadata((obj, args) => { CodeEditor target = (CodeEditor)obj; if (target.SelectionLength != (int)args.NewValue) { target.SelectionLength = (int)args.NewValue; target.Select(target.SelectionStart, (int)args.NewValue); } })); ///  /// Access to the SelectionLength property. ///  public new int SelectionLength { get { return base.SelectionLength; } set { SetValue(SelectionLengthProperty, value); } } ///  /// DependencyProperty for the TextEditor SelectionStart property. ///  public static readonly DependencyProperty SelectionStartProperty = DependencyProperty.Register("SelectionStart", typeof(int), typeof(CodeEditor), new PropertyMetadata((obj, args) => { CodeEditor target = (CodeEditor)obj; if (target.SelectionStart != (int)args.NewValue) { target.SelectionStart = (int)args.NewValue; target.Select((int)args.NewValue, target.SelectionLength); } })); ///  /// Access to the SelectionStart property. ///  public new int SelectionStart { get { return base.SelectionStart; } set { SetValue(SelectionStartProperty, value); } } #endregion // Selection. #region Properties. ///  /// The currently loaded file name. This is bound to the ViewModel /// consuming the editor control. ///  public string FilePath { get { return (string)GetValue(FilePathProperty); } set { SetValue(FilePathProperty, value); } } // Using a DependencyProperty as the backing store for FilePath. // This enables animation, styling, binding, etc... public static readonly DependencyProperty FilePathProperty = DependencyProperty.Register("FilePath", typeof(string), typeof(CodeEditor), new PropertyMetadata(String.Empty, OnFilePathChanged)); #endregion // Properties. #region Raise Property Changed. ///  /// Implement the INotifyPropertyChanged event handler. ///  public event PropertyChangedEventHandler PropertyChanged; public void RaisePropertyChanged([CallerMemberName] string caller = null) { var handler = PropertyChanged; if (handler != null) PropertyChanged(this, new PropertyChangedEventArgs(caller)); } #endregion // Raise Property Changed. } 

然后在你的视图中你想拥有AvalonEdit,你可以做到

 ...    

哪里可以放置在UserControl或Window或者什么,然后在ViewModel中我们有这个视图(我使用Caliburn Micro作为MVVM框架的东西)

  public string FilePath { get { return filePath; } set { if (filePath == value) return; filePath = value; NotifyOfPropertyChange(() => FilePath); } } ///  /// Should wrap? ///  public bool WordWrap { get { return wordWrap; } set { if (wordWrap == value) return; wordWrap = value; NotifyOfPropertyChange(() => WordWrap); } } ///  /// Display line numbers? ///  public bool ShowLineNumbers { get { return showLineNumbers; } set { if (showLineNumbers == value) return; showLineNumbers = value; NotifyOfPropertyChange(() => ShowLineNumbers); } } ///  /// Hold the start of the currently selected text. ///  private int selectionStart = 0; public int SelectionStart { get { return selectionStart; } set { selectionStart = value; NotifyOfPropertyChange(() => SelectionStart); } } ///  /// Hold the selection length of the currently selected text. ///  private int selectionLength = 0; public int SelectionLength { get { return selectionLength; } set { selectionLength = value; UpdateStatusBar(); NotifyOfPropertyChange(() => SelectionLength); } } ///  /// Gets or sets the TextLocation of the current editor control. If the /// user is setting this value it will scroll the TextLocation into view. ///  private TextLocation textLocation = new TextLocation(0, 0); public TextLocation TextLocation { get { return textLocation; } set { textLocation = value; UpdateStatusBar(); NotifyOfPropertyChange(() => TextLocation); } } 

就是这样! 完成。

我希望这有帮助。


编辑。 对于那些寻找使用MVVM使用AvalonEdit的示例的人,您可以从http://1drv.ms/1E5nhCJ下载一个非常基本的编辑器应用程序。

笔记。 该应用程序实际上通过inheritanceAvalonEdit标准控件创建MVVM友好编辑器控件,并在适当时添加其他依赖属性 – *这与我在上面给出的答案中显示的不同*。 但是,在解决方案中,我还展示了如何使用附加属性完成此操作(正如我在上面的答案中所述),并且在Behaviors命名空间下的解决方案中有代码。 然而,实际实施的是上述方法中的第一种。

另请注意,解决方案中有一些未使用的代码。 这个*示例*是一个较大的应用程序的剥离版本,我留下了一些代码,因为它对下载此示例编辑器的用户可能有用。 除了上面的内容,在示例代码中我通过绑定到文档来访问Text,有一些最纯粹的可能认为这不是纯MVVM,我说“好吧,但它有效”。 有时候打这种模式不是要走的路。

我希望这对你们中的一些人有用。

我不喜欢这些解决方案。 作者没有在Text上创建依赖项属性的原因是出于性能原因。 通过创建附加属性来解决它意味着必须在每个键击中重新创建文本字符串。 在100mb文件上,这可能是一个严重的性能问题。 在内部,它只使用文档缓冲区,除非被请求,否则永远不会创建完整的字符串。

它公开了另一个属性Document,它是一个依赖属性,它公开了Text属性,只在需要时才构造字符串。 虽然您可以绑定它,但这意味着围绕UI元素设计ViewModel会破坏与ViewModel UI无关的目的。 我也不喜欢那个选项。

老实说,最干净的(ish)解决方案是在ViewModel中创建2个事件,一个用于显示文本,另一个用于更新文本。 然后你在代码隐藏中编写一个单行事件处理程序,这很好,因为它纯粹与UI相关。 这样,只有在真正需要时才构造并分配完整的文档字符串。 此外,您甚至不需要在ViewModel中存储(也不更新)文本。 只需在需要时提升DisplayScript和UpdateScript。

这不是一个理想的解决方案,但缺点比我见过的任何其他方法都少。

TextBox也面临着类似的问题,它通过内部使用DeferredReference对象来解决它,该对象仅在真正需要时才构造字符串。 该类是内部的,不对公众可用,并且绑定代码是硬编码的,以特殊方式处理DeferredReference。 不幸的是,没有任何方法可以像TextBox一样解决问题 – 也许除非TextEditor从TextBoxinheritance。