wpf 使用 MVVM 以两种方式绑定到 AvalonEdit 文档文本
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/18964176/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Two Way Binding to AvalonEdit Document Text using MVVM
提问by MoonKnight
I want to include an AvalonEdit TextEditorcontrol into my MVVM application. The first thing I require is to be able to bind to the TextEditor.Textproperty so that I can display text. To do this I have followed and example that was given in Making AvalonEdit MVVM compatible. Now, I have implemented the following class using the accepted answer as a template
我想在TextEditor我的 MVVM 应用程序中包含一个 AvalonEdit控件。我需要的第一件事是能够绑定到TextEditor.Text属性,以便我可以显示文本。为此,我遵循了使 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));
}
}
Where the XAML is
XAML 在哪里
<Controls:MvvmTextEditor HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
FontFamily="Consolas"
FontSize="9pt"
Margin="2,2"
Text="{Binding Text, NotifyOnSourceUpdated=True, Mode=TwoWay}"/>
Firstly, this does not work. The Binding is not shown in Snoop at all (not red, not anything, in fact I cannot even see the Textdependency property).
首先,这是行不通的。Snoop 中根本没有显示绑定(不是红色,不是任何东西,事实上我什至看不到Text依赖属性)。
I have seen this question which is exactly the same as mine Two-way binding in AvalonEdit doesn't workbut the accepted answer does notwork (at least for me). So my question is:
我已经看到了这个问题,这也正是我的一样双向的AvalonEdit绑定不工作,但接受的答案确实不工作(至少对我来说)。所以我的问题是:
How can I perform two way binding using the above method and what is the correct implementation of my MvvmTextEditorclass?
如何使用上述方法执行双向绑定以及我的MvvmTextEditor类的正确实现是什么?
Thanks for your time.
谢谢你的时间。
Note: I have my Textproperty in my ViewModel and it implements the required INotifyPropertyChangedinterface.
注意:Text我的 ViewModel 中有我的属性,它实现了所需的INotifyPropertyChanged接口。
回答by 123 456 789 0
Create a Behavior class that will attach the TextChanged event and will hook up the dependency property that is bound to the ViewModel.
创建一个 Behavior 类,该类将附加 TextChanged 事件并连接绑定到 ViewModel 的依赖项属性。
AvalonTextBehavior.cs
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
查看.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>
where iis defined as "xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity""
其中i定义为 "xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity""
ViewModel.cs
视图模型.cs
private string _test;
public string Test
{
get { return _test; }
set { _test = value; }
}
That should give you the Text and push it back to the ViewModel.
这应该给你文本并将它推回 ViewModel。
回答by SlugBob
Create a BindableAvalonEditor class with a two-way binding on the Text property.
创建一个 BindableAvalonEditor 类,对 Text 属性进行双向绑定。
I was able to establish a two-way binding with the latest version of AvalonEdit by combining Jonathan Perry's answerand 123 456 789 0's answer. This allows a direct two-way binding without the need for behaviors.
通过结合Jonathan Perry's answer和123 456 789 0's answer ,我能够与最新版本的 AvalonEdit 建立双向绑定。这允许直接双向绑定而无需行为。
Here is the source code...
这是源代码...
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;
}
回答by Jonathan Perry
Another nice OOP approach is to download the source code of AvalonEdit (it's open sourced), and creating a new class that inherits from TextEditorclass (the main editor of AvalonEdit).
另一个不错的OOP 方法是下载AvalonEdit 的源代码(它是开源的),并创建一个继承自TextEditorclass(AvalonEdit 的主编辑器)的新类。
What you want to do is basically override the Textproperty and implement an INotifyPropertyChangedversion of it, using dependency property for the Textproperty and raising the OnPropertyChangedevent when text is changed (this can be done by overriding the OnTextChanged()method.
您想要做的基本上是覆盖该Text属性并实现INotifyPropertyChanged它的一个版本,使用该Text属性的依赖属性并在OnPropertyChanged更改文本时引发事件(这可以通过覆盖该OnTextChanged()方法来完成。
Here's a quick code (fully working) example that works for me:
这是一个对我有用的快速代码(完全工作)示例:
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;
}
回答by MoonKnight
For those wondering about an MVVM implementation using AvalonEdit, here is oneof the ways it can be done, first we have the class
对于那些想知道使用 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.
}
Then in your view where you want to have AvalonEdit, you can do
然后在您想要拥有 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>
Where this can be placed in a UserControl or Window or what ever, then in the ViewModel for this view we have (where I am using Caliburn Micro for the MVVM framework stuff)
这可以放置在 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);
}
}
And that's it! Done.
就是这样!完毕。
I hope this helps.
我希望这有帮助。
Edit. for all those looking for an example of working with AvalonEdit using MVVM, you can download a very basic editor application from http://1drv.ms/1E5nhCJ.
编辑。对于所有正在寻找使用 MVVM 使用 AvalonEdit 的示例的人,您可以从http://1drv.ms/1E5nhCJ下载一个非常基本的编辑器应用程序。
Notes. This application actually creates a MVVM friendly editor control by inheriting from the AvalonEdit standard control and adds additional Dependency Properties to it as appropriate - *this is different to what I have shown in the answer given above*. However, in the solution I have also shown how this can be done (as I describe in the answer above) using Attached Properties and there is code in the solution under the Behaviorsnamespace. What is actually implemented however, is the first of the above approaches.
笔记。该应用程序实际上创建了一个 MVVM 友好的编辑器控件,它继承自 AvalonEdit 标准控件,并根据需要向其添加额外的依赖属性 - *这与我在上面给出的答案中显示的不同*。但是,在解决方案中,我还展示了如何使用附加属性完成此操作(如我在上面的答案中所述),并且解决方案中的Behaviors命名空间下有代码。然而,实际实施的是上述方法中的第一种。
Please also be aware that there is some code in the solution that is unused. This *sample* was a stripped back version of a larger application and I have left some code in as it could be useful to the user who downloads this example editor. In addition to the above, in the example code I access the Text by binding to document, there are some purest that may argue that this is not pure-MVVM, and I say "okay, but it works". Some times fighting this pattern is not the way to go.
另请注意,解决方案中有一些未使用的代码。这个*示例*是一个更大应用程序的精简版本,我留下了一些代码,因为它可能对下载这个示例编辑器的用户有用。除了上述之外,在我通过绑定到文档访问文本的示例代码中,有一些最纯粹的人可能会争辩说这不是纯 MVVM,我说“好吧,但它有效”。有时,与这种模式作斗争是不可行的。
I hope this of use to some of you.
我希望这对你们中的一些人有用。
回答by Etienne Charland
I like none of these solutions. The reason the author didn't create a dependency property on Text is for performance reason. Working around it by creating an attached property means the text string must be recreated on every key stroke. On a 100mb file, this can be a serious performance issue. Internally, it only uses a document buffer and will never create the full string unless requested.
我不喜欢这些解决方案。作者没有在 Text 上创建依赖属性的原因是出于性能原因。通过创建附加属性来解决这个问题意味着必须在每次击键时重新创建文本字符串。对于 100mb 的文件,这可能是一个严重的性能问题。在内部,它只使用文档缓冲区,除非请求,否则永远不会创建完整的字符串。
It exposes another property, Document, which is a dependency property, and it exposes the Text property to construct the string only when needed. Although you can bind to it, it would mean designing your ViewModel around a UI element which defeats the purpose of having a ViewModel UI-agnostic. I don't like that option either.
它公开另一个属性 Document,它是一个依赖属性,它公开 Text 属性以仅在需要时构造字符串。尽管您可以绑定到它,但这意味着围绕 UI 元素设计您的 ViewModel,这违背了拥有与 UI 无关的 ViewModel 的目的。我也不喜欢那个选项。
Honestly, the cleanest(ish) solution is to create 2 events in your ViewModel, one to display the text and one to update the text. Then you write a one-line event handler in your code-behind, which is fine since it's purely UI-related. That way, you construct and assign the full document string only when it's truly needed. Additionally, you don't even need to store (nor update) the text in the ViewModel. Just raise DisplayScript and UpdateScript when it is needed.
老实说,最干净(ish)的解决方案是在您的 ViewModel 中创建 2 个事件,一个用于显示文本,另一个用于更新文本。然后你在你的代码隐藏中编写一个单行事件处理程序,这很好,因为它纯粹是与 UI 相关的。这样,您只在真正需要时才构造和分配完整的文档字符串。此外,您甚至不需要在 ViewModel 中存储(或更新)文本。只需在需要时提高 DisplayScript 和 UpdateScript。
It's not an ideal solution, but there are less drawbacks than any other method I've seen.
这不是一个理想的解决方案,但它的缺点比我见过的任何其他方法都要少。
TextBox also faces a similar issue, and it solves it by internally using a DeferredReference object that constructs the string only when it is really needed. That class is internal and not available to the public, and the Binding code is hard-coded to handle DeferredReference in a special way. Unfortunately there doesn't seen to be any way of solving the problem in the same way as TextBox -- perhaps unless TextEditor would inherit from TextBox.
TextBox 也面临类似的问题,它通过内部使用 DeferredReference 对象来解决它,该对象仅在真正需要时才构造字符串。该类是内部的,对公众不可用,绑定代码是硬编码的,以特殊方式处理 DeferredReference。不幸的是,没有任何方法可以像 TextBox 一样解决问题——也许除非 TextEditor 会从 TextBox 继承。

