Making AvalonEdit MVVM compatible
Asked Answered
T

3

18

I'm trying to make Avalon MVVM compatible in my WPF application. From googling, I found out that AvalonEdit is not MVVM friendly and I need to export the state of AvalonEdit by making a class derived from TextEditor then adding the necessary dependency properties. I'm afraid that I'm quite lost in Herr Grunwald's answer here:

If you really need to export the state of the editor using MVVM, then I suggest you create a class deriving from TextEditor which adds the necessary dependency properties and synchronizes them with the actual properties in AvalonEdit.

Does anyone have an example or have good suggestions on how to achieve this?

Taryntaryne answered 10/9, 2012 at 1:21 Comment(0)
C
27

Herr Grunwald is talking about wrapping the TextEditor properties with dependency properties, so that you can bind to them. The basic idea is like this (using the CaretOffset property for example):

Modified TextEditor class

public class MvvmTextEditor : TextEditor, INotifyPropertyChanged
{
    public static DependencyProperty CaretOffsetProperty = 
        DependencyProperty.Register("CaretOffset", typeof(int), typeof(MvvmTextEditor),
        // binding changed callback: set value of underlying property
        new PropertyMetadata((obj, args) =>
        {
            MvvmTextEditor target = (MvvmTextEditor)obj;
            target.CaretOffset = (int)args.NewValue;
        })
    );

    public new string Text
    {
        get { return base.Text; }
        set { base.Text = value; }
    }

    public new int CaretOffset
    {
        get { return base.CaretOffset; }
        set { base.CaretOffset = value; }
    }

    public int Length { get { return base.Text.Length; } }

    protected override void OnTextChanged(EventArgs e)
    {
        RaisePropertyChanged("Length");
        base.OnTextChanged(e);
    }

    public event PropertyChangedEventHandler PropertyChanged;
    public void RaisePropertyChanged(string info)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(info));
        }
    }
}

Now that the CaretOffset has been wrapped in a DependencyProperty, you can bind it to a property, say Offset in your View Model. For illustration, bind a Slider control's value to the same View Model property Offset, and see that when you move the Slider, the Avalon editor's cursor position gets updated:

Test XAML

<Window x:Class="AvalonDemo.TestWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit"
    xmlns:avalonExt="clr-namespace:WpfTest.AvalonExt"
    DataContext="{Binding RelativeSource={RelativeSource Self},Path=ViewModel}">
  <StackPanel>
    <avalonExt:MvvmTextEditor Text="Hello World" CaretOffset="{Binding Offset}" x:Name="editor" />
    <Slider Minimum="0" Maximum="{Binding ElementName=editor,Path=Length,Mode=OneWay}" 
        Value="{Binding Offset}" />
    <TextBlock Text="{Binding Path=Offset,StringFormat='Caret Position is {0}'}" />
    <TextBlock Text="{Binding Path=Length,ElementName=editor,StringFormat='Length is {0}'}" />
  </StackPanel>
</Window>

Test Code-behind

namespace AvalonDemo
{
    public partial class TestWindow : Window
    {
        public AvalonTestModel ViewModel { get; set; }

        public TestWindow()
        {
            ViewModel = new AvalonTestModel();
            InitializeComponent();
        }
    }
}

Test View Model

public class AvalonTestModel : INotifyPropertyChanged
{
    private int _offset;

    public int Offset 
    { 
        get { return _offset; } 
        set 
        { 
            _offset = value; 
            RaisePropertyChanged("Offset"); 
        } 
    }

    public event PropertyChangedEventHandler PropertyChanged;
    public void RaisePropertyChanged(string info)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(info));
        }
    }
}
Catching answered 13/9, 2012 at 5:11 Comment(8)
two questions, one RaisePropertyChanged is a method of AvalonTestModel but I need to call that method from my derived text editor. Do I make the method static? Also, your XAML does not compile. Slider doesn't have MinValue and MaxValue properties, and the binding did not work at all. How can I fix this?Taryntaryne
@l46kok 1. You can implement INotifyPropertyChanged on the derived text editor in the same way (not static, just a regular event). 2. The Slider properties should have been Minimum and Maximum. The code in my answer should compile now ...Catching
If I wanted to export properties such as TextArea and TextView, what needs to be done? I'm having a difficult time setting TextArea as a dependency property as it is a readonly property within TextEditorTaryntaryne
@l46kok When would you ever need to set TextArea? If there are properties inside TextArea that you need to bind to, you could set up dependency properties for those in the same way, using base.TextArea.SomeProperty.Catching
I need it for installing / uninstalling a new foldingmanager for code folding FoldingManager.Install(TextArea);Taryntaryne
I've used your idea in my application but got problem on two-way binding. I will be thankful if you take a look at my question.Quitclaim
I have implemented your suggestion in my MVVM WPF application, however when I try to read from my ViewModel the Offset, It's always 0. When I set the Offset = 2000, the TextEditor Caret moves to 2000. I'm not sure why I'm only able to set the position from the ViewModel and not read the position from the ViewModel. I've tried the Mode="TwoWay" as well as UpdateSourceTrigger="PropertyChanged" with no change to the functionalityPinnatipartite
Seems very naive to write a WPF control that doesn't support MVVM. Perhaps the better approach would be for everyone to tell the author this, and encourage him to write better components?Nabala
T
7

You can use the Document property from the editor and bind it to a property of your ViewModel.

Here is the code for the view :

<Window x:Class="AvalonEditIntegration.UI.View"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:AvalonEdit="clr-namespace:ICSharpCode.AvalonEdit;assembly=ICSharpCode.AvalonEdit"
        Title="Window1"
        WindowStartupLocation="CenterScreen"
        Width="500"
        Height="500">
    <DockPanel>
        <Button Content="Show code"
                Command="{Binding ShowCode}"
                Height="50"
                DockPanel.Dock="Bottom" />
        <AvalonEdit:TextEditor ShowLineNumbers="True"
                               Document="{Binding Path=Document}"
                               FontFamily="Consolas"
                               FontSize="10pt" />
    </DockPanel>
</Window>

And the code for the ViewModel :

namespace AvalonEditIntegration.UI
{
    using System.Windows;
    using System.Windows.Input;
    using ICSharpCode.AvalonEdit.Document;

    public class ViewModel
    {
        public ViewModel()
        {
            ShowCode = new DelegatingCommand(Show);
            Document = new TextDocument();
        }

        public ICommand ShowCode { get; private set; }
        public TextDocument Document { get; set; }

        private void Show()
        {
            MessageBox.Show(Document.Text);
        }
    }
}

source : blog nawrem.reverse

Thorwald answered 12/9, 2012 at 20:8 Comment(1)
I appreciate the effort of answering but please read the description that I have wrote in the bounty. I am aware that you can bind the Document property, but I need the entire state of the AvalonEdit (Otherwise, I'd have to be binding a lot of things starting from textview,textarea,document,visuallines, etc)Taryntaryne
R
1

Not sure if this fits your needs, but I found a way to access all the "important" components of the TextEditor on a ViewModel while having it displayed on a View, still exploring the possibilities though.

What I did was instead of instantiating the TextEditor on the View and then binding the many properties that I will need, I created a Content Control and bound its content to a TextEditor instance that I create in the ViewModel.

View:

<ContentControl Content="{Binding AvalonEditor}" />

ViewModel:

using ICSharpCode.AvalonEdit;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Highlighting;
// ...
private TextEditor m_AvalonEditor = new TextEditor();
public TextEditor AvalonEditor => m_AvalonEditor;

Test code in the ViewModel (works!)

// tests with the main component
m_AvalonEditor.SyntaxHighlighting = HighlightingManager.Instance.GetDefinition("XML");
m_AvalonEditor.ShowLineNumbers = true;
m_AvalonEditor.Load(@"C:\testfile.xml");

// test with Options
m_AvalonEditor.Options.HighlightCurrentLine = true;

// test with Text Area
m_AvalonEditor.TextArea.Opacity = 0.5;

// test with Document
m_AvalonEditor.Document.Text += "bla";

At the moment I am still deciding exactly what I need my application to configure/do with the textEditor but from these tests it seems I can change any property from it while keeping a MVVM approach.

Roentgenograph answered 5/1, 2017 at 10:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.