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

Moo*_*ght 27 c# wpf binding mvvm avalonedit

我想在TextEditor我的MVVM应用程序中包含一个AvalonEdit 控件.我要求的第一件事是能够绑定到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));
    }
}
Run Code Online (Sandbox Code Playgroud)

XAML在哪里

<Controls:MvvmTextEditor HorizontalAlignment="Stretch"
                         VerticalAlignment="Stretch"
                         FontFamily="Consolas"
                         FontSize="9pt" 
                         Margin="2,2" 
                         Text="{Binding Text, NotifyOnSourceUpdated=True, Mode=TwoWay}"/>
Run Code Online (Sandbox Code Playgroud)

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

我已经看到了这个问题,这也正是我的一样双向的AvalonEdit约束力不工作,但接受的答案并不能正常工作(至少对我来说).所以我的问题是:

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

谢谢你的时间.


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

123*_*9 0 59

创建一个将附加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;
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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>
Run Code Online (Sandbox Code Playgroud)

where 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; }
    }
Run Code Online (Sandbox Code Playgroud)

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

  • 这适用于微小的修改.在最后一段代码上......`editor.CaretOffset = editor.Document.TextLength <caretOffset?editor.Document.TextLength:caretOffset;`这将确保Caret不会超出范围. (5认同)
  • 是不是AvalonEdit开源?为什么有人只提交拉取请求以使基本控件可绑定? (4认同)
  • 从源进行更新时,这不起作用。那就是如果我设置`Test =“ XYZ”;`视图没有更新...如果我输入一些东西,它确实可以更新`Text`。 (2认同)

Eti*_*and 5

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

它公开另一个属性 Document,它是一个依赖属性,并且它公开 Text 属性以仅在需要时构造字符串。虽然您可以绑定到它,但这意味着围绕 UI 元素设计 ViewModel,这违背了 ViewModel UI 不可知的目的。我也不喜欢这个选项。

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

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

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


Slu*_*Bob 5

这是我的第一个StackOverflow帖子!

我能够使用结合了Jonathan Perry的答案123 456 789 0的答案的面向对象方法,与最新版本的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;
}
Run Code Online (Sandbox Code Playgroud)