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




string binding c# (3)

AvalonEditを使用したMVVMの実装について疑問に思っている人には、これを行う方法の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から非常に基本的なエディタアプリケーションをダウンロードできhttp://1drv.ms/1E5nhCJ.

ノート。 このアプリケーションは実際にAvalonEdit標準コントロールを継承してMVVMフレンドリなエディタコントロールを作成し、追加の依存プロパティを適切に追加します。*これは上の答えで示したものとは異なります*。 しかし、ソリューションでは、これを(添付のプロパティで説明したように)Attached Propertiesを使用してどのように行うことができるかを示し、 Behaviors名前空間のソリューションにコードがあります。 しかし、実際に実装されているのは、上記のアプローチの第1です。

また、ソリューションに未使用のコードがいくつかあることにも注意してください。 この*サンプル*は、より大きなアプリケーションの削除されたバージョンです。このサンプルエディタをダウンロードしたユーザーにとって便利なので、いくつかのコードを残しました。 上記に加えて、例のコードでは、ドキュメントにバインドしてTextにアクセスしますが、これは純粋なMVVMではないと主張するかもしれない純粋なものがあります。「大丈夫ですが動作します。 このパターンと戦ういくつかの時間は行く方法ではありません。

私はこれをあなた方の一部に願っています。

MVVMアプリケーションにAvalonEdit TextEditorコントロールを追加したいと思います。 私が必要とする最初のことは、 TextEditor.Textを表示できるように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}"/>

まず、これは機能しません。 バインドはスヌープではまったく表示されません(赤ではなく、実際にはText依存関係のプロパティも表示されません)。

私はAvalonEditでの双方向バインディングは動作しませんが、受け入れられた答えは(少なくとも私のために) うまくいきません 。 だから私の質問です:

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

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

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


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

それはあなたにテキストを与え、それをViewModelにプッシュバックする必要があります。


私はこれらの解決策のどれも好きではない。 作成者がテキストの依存関係プロパティを作成しなかったのは、パフォーマンス上の理由によるものです。 添付されたプロパティを作成することでその周りに取り組むことは、すべてのキーストロークでテキスト文字列を再作成する必要があることを意味します。 100MBのファイルでは、これは重大なパフォーマンス上の問題になる可能性があります。 内部的には、ドキュメントバッファのみを使用し、要求されない限り完全な文字列を作成しません。

これは依存プロパティであるDocumentという別のプロパティを公開し、Textプロパティを公開して必要なときにのみ文字列を構築します。 バインドすることはできますが、ViewModelをUIに依存しないようにする目的を破るUI要素の周りにViewModelを設計することを意味します。 私はその選択肢も嫌いです。

正直言って、最もクリーンな(ish)解決策は、ViewModelに2つのイベントを作成し、1つはテキストを表示し、もう1つはテキストを更新することです。 次に、コードビハインドに1行のイベントハンドラを記述します。これは純粋にUIに関連しているためです。 そうすれば、真に必要なときにのみ完全な文書ストリングを構築して割り当てることができます。 さらに、ViewModelにテキストを保存(更新も)する必要もありません。 DisplayScriptとUpdateScriptを必要なときに起動してください。

これは理想的な解決策ではありませんが、私が見た他の方法よりも欠点が少なくなっています。

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





avalonedit