Two Way Binding to AvalonEdit Document Text using MVVM
Asked Answered
I

5

32

I want to include an AvalonEdit TextEditor control into my MVVM application. The first thing I require is to be able to bind to the TextEditor.Text property 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

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

<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 Text dependency property).

I have seen this question which is exactly the same as mine Two-way binding in AvalonEdit doesn't work but the accepted answer does not work (at least for me). So my question is:

How can I perform two way binding using the above method and what is the correct implementation of my MvvmTextEditor class?

Thanks for your time.


Note: I have my Text property in my ViewModel and it implements the required INotifyPropertyChanged interface.

Insatiate answered 23/9, 2013 at 16:20 Comment(11)
Are you sure you are snooping the right control and not the underlying control template? That may be the reason why you cannot see the Text DP. I don't know how Avalon editor works but it should be similar to a RichTextBox, does the AvalonEdit doesn't have a property that it exposes when you want to grab the text inside it? If not, do you know which property was not exposed?Hairbrush
It is the Text property, the one that I am targeting. I am definitely Snooping the correct control. Thanks for your help...Insatiate
This line of code makes me suspicious though, "RaisePropertyChanged("Text");" you don't do that in the control level only in the ViewModel. You should try getting the Binding for the TextProperty and then get the binding and do UpdateSource();Hairbrush
Oh, and one more thing, change this in your dependency property, from "PropertyMetadata", "FrameworkPropertyMetadata"Hairbrush
Why change it to FrameworkPropertyMetadata? Also, can you provide an answer - it sounds like you could potentially provide a solution?Insatiate
The "RaisePropertyChanged("Text");" is changing the value of the DP, I don't see why this is suspicious?Insatiate
Because I am not sure that that is the correct answer I'm just trying to give you the right code to implement it. I want you to try it first and if it works then I'll post it as an answer. Because PropertyMetadata I believe is not going to do bindings, if you look at the constructor of the FrameworkPropertyMetadata there is a FrameworkPropertyElement that is going to be like this "new FrameworkPropertyMetadata(default(IEtsDocumentPage), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)". My next question is it looks like you created the TextProperty by yourself and not from AvalonHairbrush
Yeah, The DP is mine, but if you check the implementation it is overriding the Avalon Text property. In fact I have changed my Text property to DocumentText to avoid any confusion...Insatiate
Because it is already in the control level, it is changing from a different line of code not because of that. RaisePropertyChange will only propagate the changes back to the UI which you are right nowHairbrush
let us continue this discussion in chatHairbrush
The control is used wrong. It is meant to be modified via the TextDocument API (TextEditor.Document property). This control was obviously designed with large text in mind (hence a text editor). Editing a document means a lot of changes. Each change would create a new string object. To avoid that costs the content is abstracted away as a document that uses other ways to store text (I think they use collections of char and a StringBuilder to generate the TextDocument.Text value). I suggest using TextEditor properly. Then you get the best in terms of performance. A Text property makes no sense.Helminthiasis
H
64

Create a Behavior class that will attach the TextChanged event and will hook up the dependency property that is bound to the 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 must be defined as

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

That should give you the Text and push it back to the ViewModel.

Hairbrush answered 23/9, 2013 at 21:13 Comment(25)
This does not work when you update from source. That is if I set Test = "XYZ"; the View is not updated... It does work to update Text if I type something...Insatiate
Try adding a callback in the GiveMeTheText, and set the Text from the callback value and also implement INotifyPropertyChanged on the ViewModel and raise the event on the setter.Hairbrush
I have implemented the INotifyPropertyChanged on the Test property. I will try adding the call back...Insatiate
Looks nice, but gives an error that AssiatedObject needs an object reference (that it can't be accessed in a static context).Maritzamariupol
@lll - Thanks for updating this quick. It helped me get this going!Maritzamariupol
@III Thanks you so much for this Behavior. It just did what I needed in my mvvm eco system... colorize log files !!!! You saved me a ton of work :)Swallow
I am trying to get this done for quite sometime now, I am not successful. I have blend 2012 on my machine, but still I am unable to use Systems.Windows.Interactivity. Any help is greatly appreciated.Sulphurous
My BitBucket username is @Killercam.Insatiate
@Sulphurous You can download the working project from 1drv.ms/1zy5KUJ. The problem was that you were attempting to bind to a field that is not a DependencyProperty. For a property to allow/permit complete binding, it must be a DependencyProperty. To get around this, the easiest way was to adorn the control with a new Behavior using System.Windows.Interactivity (ExpressionBlend library and very powerful). Another way would have been to derived from TextEditor and created your own control with a bindable Text property... I hope this helps.Insatiate
@Killercam this definitely helps, but why should the field be DependencyProperty? How is interactivity help solve this problem? Please help me with more link where I can read more about these topics. Thanks a lot for your time.Sulphurous
The Interactivity class - msdn.microsoft.com/en-us/library/…. In order to bind a UI element/property via a DataContext it must be a DP. The purpose of dependency properties is to provide a way to compute the value of a property based on the value of other inputs. There is a tonne of information about this online. As for Interactivity, this was a library developed during the first major implementation of a GUI using MVVM, namely Expression Studio. Interactivity does some very sophisticated stuff, the link above will lead you to more.Insatiate
Let us continue this discussion in chat.Sulphurous
@Killercam where i can download this solution? It seems i have not access to it. My nic is Simplevolk. Thank you!Teahan
@Killercam You did help me solve the DP issue, also you did upload a project for the same. Can you please send the link for the project? I lost the solution you sent last time. I appreciate your time. The one in which you used Caliburn.Sulphurous
@Killercam: I am trying to integrate a FindReplace dialog (got this sample project from codeproject.com/Tips/768408/…) for my project which uses AvalonEdit. In my implementation I use MvvMTextEditor in xaml, how do I do FindReplaceDialog.ShowForReplace(myAvalonEditor) in my case? I use Tabs as well. Please help me. Here is the link for my project onedrive.live.com/…Sulphurous
How I have done this in the past is to create a FindReplaceViewModel/View which can be launched over the dialog. Create a class which holds the search options FindReplaceOptions.cs which has boolean caseSensitive, searchUp, matchWholeWord, regularExpression, allowWildcards, leftToRight. Then I have a FindReplaceManager.cs, this is the core class that performs find/replace operations and can be used without the FindReplace dialog. This can be useful. The manager is singleton so can be invoked from anywhere...Insatiate
This works with tiny modification. On the last code... editor.CaretOffset = editor.Document.TextLength < caretOffset? editor.Document.TextLength : caretOffset; This will make sure that Caret will not go out of bounds.Altruistic
Isn't AvalonEdit open source? Why doesn't someone just submit a pull request to make the base control bindable?Overlord
A few more comments: - If this is a two way binding way would the "Test" string in the View Model not call a Property Changed event when it is set? That way the Editor is updated if the string is changed at the View Model. - If you do a Property Changed event then I think the "PropertyChangedCallback" needs a small change to check that the incoming string is not the same as what it is already set to. If it blindly sets the Document Text then the redo command is messed up and losses its history and can not redo anything.Gainly
private static void PropertyChangedCallback(DependencyObject obj, DependencyPropertyChangedEventArgs eventArgs) { var behavior = obj as AvalonEditBehaviour; if (behavior.AssociatedObject != null) { var editor = behavior.AssociatedObject as TextEditor; if (editor.Document != null && editor.Document.Text != (string)eventArgs.NewValue) { var caretOffset = editor.CaretOffset; editor.Document.Text = eventArgs.NewValue != null ? eventArgs.NewValue.ToString() : string.Empty; editor.CaretOffset = editor.Document.TextLength < caretOffset ? editor.Document.TextLength : caretOffset; } } }Gainly
So what is that Behavior class? My Visual Studio doesn't have that (VS 2017, .NET 4.7). MS Docs suggests it is available in Blend, but I'm not using Blend but .NET/WPF.Kittle
Observed one strange behavior where PropertyChangedCallback is called before OnAttached triggers and hence Binding do not takes place the first time you open the view. How to solve it?Vargueno
Here is the fix that worked for me: protected override void OnAttached() { base.OnAttached(); if (AssociatedObject != null) { AssociatedObject.TextChanged += AssociatedObjectOnTextChanged; AssociatedObject.Text = GiveMeTheText; } }Vargueno
Is there an issue with solution initializing the TextEditor control's text from source?Puleo
The TextEdit control is designed to be modified via the TextDocument. The view model exposes a TextDocument that the TextEditor can bind to. Then in your view model class use the TextDocument API to modify the text of the document. TextEditor does not expose plain string for a good reason. This control is designed with large texts in mind (text editor). For performance reasons it does not expose the text content as string to help the user to handle the content more efficiently. If we use the TextEditor control properly there won't be any problems to solve. The API is totally ok.Helminthiasis
H
12

Create a BindableAvalonEditor class with a two-way binding on the Text property.

I was able to establish a two-way binding with the latest version of AvalonEdit by combining Jonathan Perry's answer and 123 456 789 0's answer. This allows a direct two-way binding without the need for behaviors.

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;
}
Harrietharriett answered 15/11, 2018 at 19:27 Comment(4)
This one does not seem to work - text binding is not updating source, and I don't know why.Moua
@TomCharlesZhang - Did you ever get this working with this solution?Puleo
@Puleo No, I vaguely remember there was some issue with how AvalonEdit sets up data binding in this case. My eventual solution is NOT to use data binding but to listen to text changed events instead.Moua
Why does a control implement INotifyPropertyChanged??? The whole purpose of that interface is to allow non DependencyObjects to participate in data binding. Controls can and should implement properties that participate in data binding as dependency properties. In addition, dependency properties are much faster.Helminthiasis
C
6

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.

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.

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.

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.

Chilt answered 14/7, 2018 at 5:26 Comment(3)
You don't want to store the text in the ViewModel. So the only location where the text is stored is the TextEditor in the View? How does the View request the text? By ordering the ModelView to call the DisplayScript event that contains the text as parameter? Say I've changed the text in the TextEditor. Now I want the ViewModel to do something with it, e. g. save it to a file. Will I call a method trggering the ViewModel to call the UpdateScript event? Does that event have a reference parameter in which the View will set the TextReader's text?Formula
The View doesn't request the Text. The VIewModel sets the text when loading the file. If the ViewModel wants to save to a file, then it requests the text from the UI. For the events, I have an argument type as a class with 2 fields: the ViewModel associated with the TextEditor (or any reference object if there are multiple editors), and a Text property to read or set the text. Calling a method on the ViewModel that is triggering back an event on the UI makes no sense. It's actually very simple. The ViewModel knows when it needs to load or save.Chilt
This is the only reasonable answer. All other answers are of low quality from a technical point of view. Controls that implement INotifyPropertyChanged etc. If we want to manipulate the text from the view model then we have to do it via the TextDocument object that binds to the TextEditor. It's that simple. Instead of hacking together some dirty code it's wiser to understand the APIs and use the classes properly. then there is no problem that "needs" to be solved. Those problems are created artificially based on ignorance.Helminthiasis
P
3

Another nice OOP approach is to download the source code of AvalonEdit (it's open sourced), and creating a new class that inherits from TextEditor class (the main editor of AvalonEdit).

What you want to do is basically override the Text property and implement an INotifyPropertyChanged version of it, using dependency property for the Text property and raising the OnPropertyChanged event when text is changed (this can be done by overriding the OnTextChanged() method.

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;
}
Perihelion answered 29/12, 2013 at 9:53 Comment(4)
This does not work for edits back to the bound property. (I used the following binding: Text="{Binding CurrentText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}")Maritzamariupol
However, it does work great in being able to set text to a value and have it get into AvalonEdit.Maritzamariupol
@Vaccano: How did you get the AvalonEditor Text for two way binding? I am able to view the text, but when I update the ViewModel does not get updated. Any thoughts?Sulphurous
Why does a control implement INotifyPropertyChanged??? The whole purpose of that interface is to allow non DependencyObjects to participate in data binding. Controls can and should implement properties that participate in data binding as dependency properties. In addition, dependency properties are much faster.Helminthiasis
I
3

For those wondering about an MVVM implementation using AvalonEdit, here is one of the ways it can be done, first we have the class

/// <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

...
<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)

    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.

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 Behaviors namespace. What is actually implemented however, is the first of the above approaches.

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.

I hope this of use to some of you.

Insatiate answered 25/1, 2015 at 14:58 Comment(8)
How to get the caret position linked to a label in status bar?Sulphurous
I have two issues one is the Cursor caret issue, when you run the program and open a .seg file, it throws an exception. The second problem is when I have multiple files open and when I want to print the current active tab/file, how do I implement PrintCommand? Here is the link where you can download the code bitbucket.org/sahanatambi/avaloneditor/overview Thanks for your patience.Sulphurous
Hi, I have looked at your code and to be honest, it was going to be quicker for me to provide an example of how this can be done from scratch, so that is what I have done. You don't need to use an MVVM framework, but that it what I have done, I have also included MahApps Metro as it looks cool. Now, Please do not worry about "Bootstrap" et al., all you need to look at is "MvvmTextEditor" and how this is incorporated in to my nice VS2012 style tab control. I am basically linking from the active Editor Control to the MainWindow ("Shell") so that we gat a nice and consistent UI updates...Insatiate
I have not looked at your print problem as this is a totally different issue and one you will be better off asking a new question for. If you ask the question, you can link it here and I might take a look for you. Firstly, I would like to say congrats on you MVVM efforts so far, doing this from scratch (without MVVM framework) is not easy, but will make you much better in the long run. I created an app from scratch without a framework when I first started. If I could offer any advice at this point, it would be to not over complicate your inheritance hierarchies...Insatiate
If you need to have one editor control inherit from another fair enough, but I rarely see a need for this, creating a new control with specific behavior based on the MvvmTextEditor (you will see what I mean) is much cleaner in most cases. The link to the project is 1drv.ms/1E5nhCJ I hope you find it useful. Please let me know when you have downloaded this and I will remove it... I hope this helps. Good luck.Insatiate
I did download, yeah let me take a look. Thank you so much for your effort.Sulphurous
Let us continue this discussion in chat.Sulphurous
Why does a control implement INotifyPropertyChanged??? The whole purpose of that interface is to allow non DependencyObjects to participate in data binding. CodeEditor must at least extend DependencyObject, which it implicitly does as it extends TextEditor. It makes absolutely no sense to implement properties on a control using INotifyPropertyChanged. It's also far slower than dependency objects and they can't accept a binding.Helminthiasis

© 2022 - 2024 — McMap. All rights reserved.