30

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.

Community
  • 1
  • 1
MoonKnight
  • 23,430
  • 34
  • 134
  • 249
  • 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? – 123 456 789 0 Sep 23 '13 at 17:24
  • It is the `Text` property, the one that I am targeting. I am definitely Snooping the correct control. Thanks for your help... – MoonKnight Sep 23 '13 at 19:43
  • 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(); – 123 456 789 0 Sep 23 '13 at 20:01
  • Oh, and one more thing, change this in your dependency property, from "PropertyMetadata", "FrameworkPropertyMetadata" – 123 456 789 0 Sep 23 '13 at 20:01
  • Why change it to `FrameworkPropertyMetadata`? Also, can you provide an answer - it sounds like you could potentially provide a solution? – MoonKnight Sep 23 '13 at 20:05
  • The "RaisePropertyChanged("Text");" is changing the value of the DP, I don't see why this is suspicious? – MoonKnight Sep 23 '13 at 20:07
  • 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 Avalon – 123 456 789 0 Sep 23 '13 at 20:08
  • 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... – MoonKnight Sep 23 '13 at 20:10
  • 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 now – 123 456 789 0 Sep 23 '13 at 20:11
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/37904/discussion-between-leo-lorenzo-luis-and-killercam) – 123 456 789 0 Sep 23 '13 at 20:11

5 Answers5

62

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>

where i is 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.

MgSam
  • 10,330
  • 16
  • 59
  • 85
123 456 789 0
  • 10,240
  • 4
  • 40
  • 68
  • 2
    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... – MoonKnight Sep 23 '13 at 22:12
  • 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. – 123 456 789 0 Sep 23 '13 at 22:17
  • I have implemented the `INotifyPropertyChanged` on the `Test` property. I will try adding the call back... – MoonKnight Sep 23 '13 at 22:19
  • 1
    Looks nice, but gives an error that `AssiatedObject` needs an object reference (that it can't be accessed in a static context). – Vaccano Feb 19 '14 at 20:53
  • @lll - Thanks for updating this quick. It helped me get this going! – Vaccano Feb 19 '14 at 21:06
  • @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 :) – Pascal Jun 18 '14 at 10:32
  • 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. – savi Jan 23 '15 at 17:10
  • 1
    My BitBucket username is @Killercam. – MoonKnight Jan 29 '15 at 10:02
  • 1
    @savi You can download the working project from http://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. – MoonKnight Feb 02 '15 at 20:28
  • @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. – savi Feb 02 '15 at 22:24
  • 1
    The Interactivity class - https://msdn.microsoft.com/en-us/library/system.windows.interactivity%28v=expression.40%29.aspx. 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. – MoonKnight Feb 02 '15 at 22:48
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/70073/discussion-between-savi-and-killercam). – savi Feb 03 '15 at 00:36
  • @Killercam where i can download this solution? It seems i have not access to it. My nic is Simplevolk. Thank you! – user2545071 Mar 23 '15 at 07:45
  • @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. – savi May 08 '15 at 01:06
  • @Killercam: I am trying to integrate a FindReplace dialog (got this sample project from http://www.codeproject.com/Tips/768408/A-Find-and-Replace-Tool-for-AvalonEdit) 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 https://onedrive.live.com/redir?resid=d81115223852f84d!107&authkey=!AANpEgjWTQO8lAU&ithint=file%2czip – savi May 12 '15 at 23:42
  • 1
    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... – MoonKnight May 13 '15 at 09:08
  • @Killercam Thanks a lot! I appreciate your time :) – savi May 13 '15 at 17:01
  • 5
    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. – Marc Vitalis Oct 14 '15 at 03:44
  • 4
    Isn't AvalonEdit open source? Why doesn't someone just submit a pull request to make the base control bindable? – MgSam Nov 09 '15 at 16:16
  • 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. – Tommy Feb 15 '18 at 00:15
  • 1
    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; } } } – Tommy Feb 15 '18 at 00:23
  • 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. – ygoe Feb 26 '19 at 14:08
  • 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? – Furqan Safdar Mar 20 '20 at 09:27
  • Here is the fix that worked for me: protected override void OnAttached() { base.OnAttached(); if (AssociatedObject != null) { AssociatedObject.TextChanged += AssociatedObjectOnTextChanged; AssociatedObject.Text = GiveMeTheText; } } – Furqan Safdar Mar 20 '20 at 10:06
8

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;
}
SlugBob
  • 81
  • 1
  • 4
4

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;
}
Jonathan Perry
  • 2,685
  • 1
  • 38
  • 46
  • 4
    This does not work for edits back to the bound property. (I used the following binding: `Text="{Binding CurrentText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"`) – Vaccano Feb 19 '14 at 20:35
  • 1
    However, it does work great in being able to set text to a value and have it get into AvalonEdit. – Vaccano Feb 19 '14 at 20:36
  • 3
    @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? – savi Aug 28 '14 at 17:09
4

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.

MoonKnight
  • 23,430
  • 34
  • 134
  • 249
  • How to get the caret position linked to a label in status bar? – savi Feb 06 '15 at 19:24
  • 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 https://bitbucket.org/sahanatambi/avaloneditor/overview Thanks for your patience. – savi Feb 10 '15 at 01:44
  • 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... – MoonKnight Feb 10 '15 at 23:41
  • 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... – MoonKnight Feb 10 '15 at 23:45
  • 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 http://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. – MoonKnight Feb 10 '15 at 23:46
  • I did download, yeah let me take a look. Thank you so much for your effort. – savi Feb 11 '15 at 00:30
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/70672/discussion-between-savi-and-killercam). – savi Feb 11 '15 at 00:56
3

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.

Etienne Charland
  • 2,175
  • 2
  • 17
  • 37
  • 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? – dutop Jul 17 '18 at 10:18
  • 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. – Etienne Charland Jul 25 '18 at 17:41