使用 MVVM 两种方式绑定到 AvalonEdit 文档文本
Posted
技术标签:
【中文标题】使用 MVVM 两种方式绑定到 AvalonEdit 文档文本【英文标题】:Two Way Binding to AvalonEdit Document Text using MVVM 【发布时间】:2013-09-28 15:21:45 【问题描述】:我想在我的 MVVM 应用程序中包含一个 AvalonEdit TextEditor
控件。我需要的第一件事是能够绑定到TextEditor.Text
属性,以便我可以显示文本。为此,我遵循了Making AvalonEdit MVVM compatible 中给出的示例。现在,我已经使用接受的答案作为模板实现了以下类
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 在哪里
<Controls:MvvmTextEditor HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
FontFamily="Consolas"
FontSize="9pt"
Margin="2,2"
Text="Binding Text, NotifyOnSourceUpdated=True, Mode=TwoWay"/>
首先,这不起作用。绑定根本没有显示在 Snoop 中(不是红色,什么都没有,事实上我什至看不到 Text
依赖属性)。
我看到这个问题与我的Two-way binding in AvalonEdit doesn't work 完全相同,但接受的答案确实不 工作(至少对我来说)。所以我的问题是:
如何使用上述方法执行双向绑定,我的MvvmTextEditor
类的正确实现是什么?
感谢您的宝贵时间。
注意:我的 ViewModel 中有 Text
属性,它实现了所需的 INotifyPropertyChanged
接口。
【问题讨论】:
您确定您正在窥探正确的控件而不是底层控件模板吗?这可能是您看不到 Text DP 的原因。我不知道 Avalon 编辑器是如何工作的,但它应该类似于 RichTextBox,当您想要获取其中的文本时,AvalonEdit 是否没有它公开的属性?如果没有,你知道哪个属性没有暴露吗? 这是Text
属性,我的目标。我绝对是在窥探正确的控制。感谢您的帮助...
这行代码让我很怀疑,"RaisePropertyChanged("Text");"您不能仅在 ViewModel 的控制级别中执行此操作。您应该尝试获取 TextProperty 的绑定,然后获取绑定并执行 UpdateSource();
哦,还有一件事,改变你的依赖属性,从“PropertyMetadata”,“FrameworkPropertyMetadata”
为什么要改成FrameworkPropertyMetadata
?另外,你能提供一个答案吗?听起来你可能会提供一个解决方案?
【参考方案1】:
创建一个 Behavior 类,该类将附加 TextChanged 事件并连接绑定到 ViewModel 的依赖项属性。
AvalonTextBehavior.cs
public sealed class AvalonEditBehaviour : Behavior<TextEditor>
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
<avalonedit:TextEditor
WordWrap="True"
ShowLineNumbers="True"
LineNumbersForeground="Magenta"
x:Name="textEditor"
FontFamily="Consolas"
SyntaxHighlighting="XML"
FontSize="10pt">
<i:Interaction.Behaviors>
<controls:AvalonEditBehaviour GiveMeTheText="Binding Test, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged"/>
</i:Interaction.Behaviors>
</avalonedit:TextEditor>
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。
【讨论】:
从源更新时这不起作用。也就是说,如果我设置Test = "XYZ";
,则视图未更新...如果我输入某些内容,它确实可以更新Text
...
看起来不错,但给出了一个错误,AssiatedObject
需要一个对象引用(它不能在静态上下文中访问)。
我的 BitBucket 用户名是 @Killercam。
这适用于微小的修改。在最后一个代码上... editor.CaretOffset = editor.Document.TextLength < caretOffset? editor.Document.TextLength : caretOffset;
这将确保插入符号不会超出范围。
AvalonEdit 不是开源的吗?为什么不提交一个拉取请求以使基本控件可绑定?【参考方案2】:
在 Text 属性上创建一个双向绑定的 BindableAvalonEditor 类。
通过结合Jonathan Perry's answer 和123 456 789 0's answer,我能够与最新版本的 AvalonEdit 建立双向绑定。这允许直接双向绑定而无需行为。
这里是源代码...
public class BindableAvalonEditor : ICSharpCode.AvalonEdit.TextEditor, INotifyPropertyChanged
/// <summary>
/// A bindable Text property
/// </summary>
public new string Text
get
return (string)GetValue(TextProperty);
set
SetValue(TextProperty, value);
RaisePropertyChanged("Text");
/// <summary>
/// The bindable text property dependency property
/// </summary>
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(
"Text",
typeof(string),
typeof(BindableAvalonEditor),
new FrameworkPropertyMetadata
DefaultValue = default(string),
BindsTwoWayByDefault = true,
PropertyChangedCallback = OnDependencyPropertyChanged
);
protected static void OnDependencyPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
var target = (BindableAvalonEditor)obj;
if (target.Document != null)
var caretOffset = target.CaretOffset;
var newValue = args.NewValue;
if (newValue == null)
newValue = "";
target.Document.Text = (string)newValue;
target.CaretOffset = Math.Min(caretOffset, newValue.ToString().Length);
protected override void OnTextChanged(EventArgs e)
if (this.Document != null)
Text = this.Document.Text;
base.OnTextChanged(e);
/// <summary>
/// Raises a property changed event
/// </summary>
/// <param name="property">The name of the property that updates</param>
public void RaisePropertyChanged(string property)
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
public event PropertyChangedEventHandler PropertyChanged;
【讨论】:
【参考方案3】:另一个不错的 OOP 方法是下载 AvalonEdit 的源代码(它是开源的),并创建一个继承自 TextEditor
类(AvalonEdit 的主编辑器)的新类。
您想要做的基本上是覆盖Text
属性并实现它的INotifyPropertyChanged
版本,使用Text
属性的依赖属性并在文本更改时引发OnPropertyChanged
事件(这可以是通过覆盖OnTextChanged()
方法完成。
这是一个适用于我的快速代码(完全工作)示例:
public class BindableTextEditor : TextEditor, INotifyPropertyChanged
/// <summary>
/// A bindable Text property
/// </summary>
public new string Text
get return base.Text;
set base.Text = value;
/// <summary>
/// The bindable text property dependency property
/// </summary>
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);
/// <summary>
/// Raises a property changed event
/// </summary>
/// <param name="property">The name of the property that updates</param>
public void RaisePropertyChanged(string property)
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
public event PropertyChangedEventHandler PropertyChanged;
【讨论】:
这不适用于编辑回绑定属性。 (我使用了以下绑定:Text="Binding CurrentText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged"
)
然而,它在将文本设置为一个值并让它进入 AvalonEdit 方面确实非常有效。
@Vaccano:你是如何获得用于双向绑定的 AvalonEditor 文本的?我可以查看文本,但是当我更新 ViewModel 时不会更新。有什么想法吗?【参考方案4】:
对于那些想知道使用 AvalonEdit 实现 MVVM 的人,这里是一种可以完成的方法,首先我们有类
/// <summary>
/// Class that inherits from the AvalonEdit TextEditor control to
/// enable MVVM interaction.
/// </summary>
public class CodeEditor : TextEditor, INotifyPropertyChanged
// Vars.
private static bool canScroll = true;
/// <summary>
/// Default constructor to set up event handlers.
/// </summary>
public CodeEditor()
// Default options.
FontSize = 12;
FontFamily = new FontFamily("Consolas");
Options = new TextEditorOptions
IndentationSize = 3,
ConvertTabsToSpaces = true
;
#region Text.
/// <summary>
/// Dependancy property for the editor text property binding.
/// </summary>
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;
));
/// <summary>
/// Provide access to the Text.
/// </summary>
public new string Text
get return base.Text;
set base.Text = value;
/// <summary>
/// Return the current text length.
/// </summary>
public int Length
get return base.Text.Length;
/// <summary>
/// Override of OnTextChanged event.
/// </summary>
protected override void OnTextChanged(EventArgs e)
RaisePropertyChanged("Length");
base.OnTextChanged(e);
/// <summary>
/// Event handler to update properties based upon the selection changed event.
/// </summary>
void TextArea_SelectionChanged(object sender, EventArgs e)
this.SelectionStart = SelectionStart;
this.SelectionLength = SelectionLength;
/// <summary>
/// Event that handles when the caret changes.
/// </summary>
void TextArea_CaretPositionChanged(object sender, EventArgs e)
try
canScroll = false;
this.TextLocation = TextLocation;
finally
canScroll = true;
#endregion // Text.
#region Caret Offset.
/// <summary>
/// DependencyProperty for the TextEditorCaretOffset binding.
/// </summary>
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;
));
/// <summary>
/// Access to the SelectionStart property.
/// </summary>
public new int CaretOffset
get return base.CaretOffset;
set SetValue(CaretOffsetProperty, value);
#endregion // Caret Offset.
#region Selection.
/// <summary>
/// DependencyProperty for the TextLocation. Setting this value
/// will scroll the TextEditor to the desired TextLocation.
/// </summary>
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);
));
/// <summary>
/// Get or set the TextLocation. Setting will scroll to that location.
/// </summary>
public TextLocation TextLocation
get return base.Document.GetLocation(SelectionStart);
set SetValue(TextLocationProperty, value);
/// <summary>
/// DependencyProperty for the TextEditor SelectionLength property.
/// </summary>
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);
));
/// <summary>
/// Access to the SelectionLength property.
/// </summary>
public new int SelectionLength
get return base.SelectionLength;
set SetValue(SelectionLengthProperty, value);
/// <summary>
/// DependencyProperty for the TextEditor SelectionStart property.
/// </summary>
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);
));
/// <summary>
/// Access to the SelectionStart property.
/// </summary>
public new int SelectionStart
get return base.SelectionStart;
set SetValue(SelectionStartProperty, value);
#endregion // Selection.
#region Properties.
/// <summary>
/// The currently loaded file name. This is bound to the ViewModel
/// consuming the editor control.
/// </summary>
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.
/// <summary>
/// Implement the INotifyPropertyChanged event handler.
/// </summary>
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 的视图中,你可以这样做
...
<Grid>
<Local:CodeEditor
x:Name="CodeEditor"
FilePath="Binding FilePath,
Mode=TwoWay,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True"
WordWrap="Binding WordWrap,
Mode=TwoWay,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True"
ShowLineNumbers="Binding ShowLineNumbers,
Mode=TwoWay,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True"
SelectionLength="Binding SelectionLength,
Mode=TwoWay,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True"
SelectionStart="Binding SelectionStart,
Mode=TwoWay,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True"
TextLocation="Binding TextLocation,
Mode=TwoWay,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True"/>
</Grid>
可以将其放置在 UserControl 或 Window 或其他任何地方,然后在 ViewModel 中为我们拥有的这个视图(我使用 Caliburn Micro 作为 MVVM 框架的东西)
public string FilePath
get return filePath;
set
if (filePath == value)
return;
filePath = value;
NotifyOfPropertyChange(() => FilePath);
/// <summary>
/// Should wrap?
/// </summary>
public bool WordWrap
get return wordWrap;
set
if (wordWrap == value)
return;
wordWrap = value;
NotifyOfPropertyChange(() => WordWrap);
/// <summary>
/// Display line numbers?
/// </summary>
public bool ShowLineNumbers
get return showLineNumbers;
set
if (showLineNumbers == value)
return;
showLineNumbers = value;
NotifyOfPropertyChange(() => ShowLineNumbers);
/// <summary>
/// Hold the start of the currently selected text.
/// </summary>
private int selectionStart = 0;
public int SelectionStart
get return selectionStart;
set
selectionStart = value;
NotifyOfPropertyChange(() => SelectionStart);
/// <summary>
/// Hold the selection length of the currently selected text.
/// </summary>
private int selectionLength = 0;
public int SelectionLength
get return selectionLength;
set
selectionLength = value;
UpdateStatusBar();
NotifyOfPropertyChange(() => SelectionLength);
/// <summary>
/// Gets or sets the TextLocation of the current editor control. If the
/// user is setting this value it will scroll the TextLocation into view.
/// </summary>
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.
下载一个非常基本的编辑器应用程序注释。这个应用程序实际上通过从 AvalonEdit 标准控件继承来创建一个 MVVM 友好的编辑器控件,并在适当时向它添加额外的依赖属性 - *这与我在上面给出的答案中显示的不同*。但是,在解决方案中,我还展示了如何使用附加属性来完成此操作(正如我在上面的答案中所描述的那样),并且解决方案中有 Behaviors
命名空间下的代码。然而,实际实现的是上述方法中的第一种。
还请注意,解决方案中有一些未使用的代码。这个 *sample* 是一个大型应用程序的精简版本,我留下了一些代码,因为它可能对下载此示例编辑器的用户有用。除了上述之外,在我通过绑定到文档来访问文本的示例代码中,有一些最纯粹的人可能会争辩说这不是纯 MVVM,我说“好的,但它有效”。有时与这种模式作斗争并不是可行的方法。
我希望这对你们中的一些人有用。
【讨论】:
如何让插入符号位置链接到状态栏中的标签? 我有两个问题,一个是光标插入符号问题,当您运行程序并打开 .seg 文件时,它会引发异常。第二个问题是当我打开多个文件并且想要打印当前活动的选项卡/文件时,如何实现 PrintCommand?这是您可以下载代码bitbucket.org/sahanatambi/avaloneditor/overview的链接,感谢您的耐心等待。 嗨,我查看了您的代码,老实说,提供一个如何从头开始完成的示例对我来说会更快,所以这就是我所做的。你不需要使用 MVVM 框架,但是我已经完成了,我还包括了 MahApps Metro,因为它看起来很酷。现在,请不要担心“Bootstrap”等,您只需要查看“MvvmTextEditor”以及如何将其合并到我漂亮的 VS2012 样式选项卡控件中。我基本上是从活动的编辑器控件链接到 MainWindow(“Shell”),以便我们获得良好且一致的 UI 更新... 我没有查看您的打印问题,因为这是一个完全不同的问题,您最好提出一个新问题。如果你问这个问题,你可以在这里链接,我可能会帮你看看。首先,我要恭喜你到目前为止在 MVVM 方面所做的努力,从头开始(没有 MVVM 框架)做这件事并不容易,但从长远来看会让你变得更好。当我第一次开始时,我从头开始创建了一个没有框架的应用程序。如果我可以在这一点上提供任何建议,那就是不要让你的继承层次结构过于复杂...... 如果您需要一个编辑器控件从另一个继承自另一个足够公平,但我很少看到需要这样做,基于 MvvmTextEditor 创建一个具有特定行为的新控件(你会明白我的意思)在大多数情况下更清洁。该项目的链接是1drv.ms/1E5nhCJ,希望对您有用。下载后请告诉我,我将删除它...我希望这会有所帮助。祝你好运。【参考方案5】:我不喜欢这些解决方案。作者没有在 Text 上创建依赖属性的原因是出于性能原因。通过创建附加属性来解决它意味着必须在每次击键时重新创建文本字符串。对于 100mb 的文件,这可能是一个严重的性能问题。在内部,它只使用文档缓冲区,除非请求,否则永远不会创建完整的字符串。
它公开了另一个属性 Document,它是一个依赖属性,并且它公开了 Text 属性以仅在需要时构造字符串。尽管您可以绑定到它,但这意味着围绕 UI 元素设计您的 ViewModel,这违背了 ViewModel UI 不可知论的目的。我也不喜欢那个选项。
老实说,最干净(ish)的解决方案是在您的 ViewModel 中创建 2 个事件,一个用于显示文本,一个用于更新文本。然后,您在代码隐藏中编写一个单行事件处理程序,这很好,因为它纯粹与 UI 相关。这样,您仅在真正需要时才构造和分配完整的文档字符串。此外,您甚至不需要在 ViewModel 中存储(或更新)文本。只需在需要时引发 DisplayScript 和 UpdateScript。
这不是一个理想的解决方案,但缺点比我见过的任何其他方法都少。
TextBox 也面临类似的问题,它通过在内部使用 DeferredReference 对象来解决这个问题,该对象仅在真正需要时才构造字符串。该类是内部的,对公众不可用,并且绑定代码是硬编码的,以便以特殊方式处理 DeferredReference。不幸的是,没有任何方法可以像 TextBox 一样解决问题——也许除非 TextEditor 继承自 TextBox。
【讨论】:
您不想将文本存储在 ViewModel 中。那么存储文本的唯一位置是视图中的 TextEditor 吗?视图如何请求文本?通过命令 ModelView 调用包含文本作为参数的 DisplayScript 事件?假设我更改了 TextEditor 中的文本。现在我希望 ViewModel 对它做一些事情,例如。 G。将其保存到文件中。我会调用一个触发 ViewModel 的方法来调用 UpdateScript 事件吗?该事件是否有一个引用参数,视图将在其中设置 TextReader 的文本? 视图不请求文本。 VIewModel 在加载文件时设置文本。如果 ViewModel 想要保存到文件,那么它会从 UI 请求文本。对于事件,我有一个作为具有 2 个字段的类的参数类型:与 TextEditor 关联的 ViewModel(或任何引用对象,如果有多个编辑器),以及一个用于读取或设置文本的 Text 属性。在 ViewModel 上调用触发 UI 上的事件的方法是没有意义的。其实很简单。 ViewModel 知道何时需要加载或保存。以上是关于使用 MVVM 两种方式绑定到 AvalonEdit 文档文本的主要内容,如果未能解决你的问题,请参考以下文章
Vue 注意事项 模板语法 单双向绑定 语法格式 MVVM框架 Object.defineProperty和数据代理操作
Xamarin Forms 两种方式绑定ActivityIndicator