web-dev-qa-db-ja.com

MVVMを使用したAvalonEditドキュメントテキストへの双方向バインディング

AvalonEdit TextEditorコントロールをMVVMアプリケーションに含めたいのですが。最初に必要なのは、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));
    }
}

XAMLの場所

<Controls:MvvmTextEditor HorizontalAlignment="Stretch"
                         VerticalAlignment="Stretch"
                         FontFamily="Consolas"
                         FontSize="9pt" 
                         Margin="2,2" 
                         Text="{Binding Text, NotifyOnSourceUpdated=True, Mode=TwoWay}"/>

まず、これは機能しません。 BindingはSnoopにはまったく表示されません(赤ではなく、何も表示されません。実際、Text依存関係プロパティも表示されません)。

私の質問とまったく同じこの質問を見ました AvalonEditの双方向バインディングは機能しません が、受け入れられた回答は機能しません機能します(at少なくとも私にとっては)。だから私の質問は:

上記の方法を使用して双方向バインディングを実行するにはどうすればよいですか?MvvmTextEditorクラスの正しい実装は何ですか?

御時間ありがとうございます。


注:私のTextプロパティはViewModelにあり、必要なINotifyPropertyChangedインターフェースを実装しています。

28
MoonKnight

TextChangedイベントをアタッチし、ViewModelにバインドされている依存関係プロパティをフックするBehaviorクラスを作成します。

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; }
    }

これでテキストが表示され、ViewModelにプッシュされます。

58
123 456 789 0

AvalonEditを使用したMVVM実装について不思議に思う人のために、これがoneの実行方法の1つです。最初にクラスを用意します。

/// <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に配置します(MVVMフレームワーク用にCaliburn Microを使用しています)。

    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ではないと主張する可能性のある最も純粋なものがいくつかあります。時々、このパターンと戦うのはうまくいかないことがあります。

これがあなたの一部に役立つことを願っています。

4
MoonKnight

これが私の最初の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;
}
4
SlugBob

別のニース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;
}
4
Jonathan Perry

これらのソリューションはどれも好きではありません。著者がTextに依存関係プロパティを作成しなかった理由は、パフォーマンス上の理由によるものです。添付プロパティを作成して回避するには、キーストロークごとにテキスト文字列を再作成する必要があります。 100mbファイルでは、これは深刻なパフォーマンスの問題になる可能性があります。内部的には、ドキュメントバッファーのみを使用し、要求されない限り完全な文字列を作成することはありません。

これは、依存関係プロパティである別のプロパティDocumentを公開し、Textプロパティを公開して、必要な場合にのみ文字列を構築します。バインドすることはできますが、UI要素を中心にViewModelを設計することになるため、UIに依存しないViewModelを使用するという目的に反することになります。そのオプションも好きではありません。

正直なところ、最もクリーンな(ish)ソリューションは、ViewModelで2つのイベントを作成することです。1つはテキストを表示し、もう1つはテキストを更新します。次に、コードビハインドに1行のイベントハンドラーを記述します。これは、純粋にUI関連であるため問題ありません。このようにして、本当に必要な場合にのみ、完全なドキュメント文字列を作成して割り当てます。さらに、テキストをViewModelに保存(または更新)する必要さえありません。必要なときにDisplayScriptとUpdateScriptを呼び出すだけです。

これは理想的なソリューションではありませんが、私が見た他のどの方法よりも欠点は少ないです。

TextBoxも同様の問題に直面し、本当に必要な場合にのみ文字列を構築するDeferredReferenceオブジェクトを内部的に使用することで解決します。そのクラスは内部にあり、パブリックに使用できません。Bindingコードは、特別な方法でDeferredReferenceを処理するようにハードコードされています。残念ながら、おそらくTextEditorがTextBoxから継承しない限り、TextBoxと同じ方法で問題を解決する方法はないようです。

1