我想在我的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
接口。
答案 0 :(得分:57)
创建一个将附加TextChanged事件的Behavior类,并将挂钩绑定到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。
答案 1 :(得分:4)
另一个不错的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;
}
答案 2 :(得分: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
命名空间下的解决方案中有代码。然而,实际实施的是上述第一种方法。
请注意,解决方案中有一些未使用的代码。这个*示例*是一个较大的应用程序的剥离版本,我留下了一些代码,因为它对下载此示例编辑器的用户可能有用。除了上面的内容,在示例代码中我通过绑定到文档来访问Text,有一些最纯粹的可能认为这不是纯MVVM,我说“好吧,但它有效”。有时候打这种模式不是可行的方法。
我希望对你们这些人有用。
答案 3 :(得分:3)
这是我的第一个StackOverflow帖子!
我能够使用结合了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;
}
答案 4 :(得分:0)
我不喜欢这些解决方案。作者之所以没有在Text上创建依赖项属性,是出于性能方面的考虑。通过创建附加属性来解决该问题,这意味着必须在每次击键时都重新创建文本字符串。在100mb的文件上,这可能是一个严重的性能问题。在内部,它仅使用文档缓冲区,除非有要求,否则绝不会创建完整的字符串。
它公开了另一个属性Document(一个依赖项属性),并且公开了Text属性以仅在需要时构造字符串。尽管您可以绑定到它,但这将意味着围绕UI元素设计ViewModel,这违背了与ViewModel UI无关的目的。我也不喜欢那个选项。
老实说,最干净的解决方案是在ViewModel中创建2个事件,一个用于显示文本,另一个用于更新文本。然后,在您的代码后面编写一个单行事件处理程序,这很好,因为它与UI完全相关。这样,仅在真正需要时才构造和分配完整的文档字符串。另外,您甚至不需要在ViewModel中存储(或更新)文本。只需在需要时提高DisplayScript和UpdateScript。
这不是理想的解决方案,但是缺点比我见过的任何其他方法都要少。
TextBox也面临类似的问题,它通过内部使用DeferredReference对象来解决该问题,该对象仅在确实需要时才构造字符串。该类是内部的,并不对公众开放,并且绑定代码经过硬编码以特殊方式处理DeferredReference。不幸的是,似乎没有任何方法可以像TextBox一样解决问题-除非TextEditor将从TextBox继承。