2

I am currently working on a project where users will be entering C# scripts that get dynamically compiled and executed. I will skip giving more details for now as I will probably have questions about other aspects of the project later.

Due to some higher level decisions that are out of my control, the user interface will be developed using WPF and the Quantum Whale Editor.NET control will be used as the editor. Sadly, it seems that the WPF version of the QWhale Editor.NET is still not fully mature and so it is lacking documentations and, worst, it does not seem to be binding friendly.

While I am still new to WPF, I am somewhat familiar with MVVM and so I would love to apply that. However, I was faced with the first challenge when I was testing the evaluation version and tried binding the editor's text to a property of a model and received an exception that it is not possible:

A 'Binding' cannot be set on the 'Text' property of type 'TextEditor'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject.

Having tried AvalonEdit while checking the alternatives, I remembered this other Stack Overflow question: Making AvalonEdit MVVM Compatible. And so I followed the same concept.

I have defined a class that inherits from the editor, added a dependency property and at first tried combining that with hiding the original Text property of the editor using new. But obviously that was a long shot and my property was not used, the base property was being called directly.

Once that failed, I defined a brand new property called DocumentText. I had it wrap base.Text, defined the binding using it and accordingly got one direction of binding working. That being from the model to the control. But from what I found, the best way to get binding working in the other direction is to override the OnTextChanged event (or an equivalent) to have it raise the property changed notification. The problem is that the control does not have such an event, as weird as that sounds.

Now I could probably override a bunch of other events (e.g. OnKeyUp, OnMouseClick, etc.) so that I could handle all possible actions that could modify the text (typing, drag & dropping, pasting, etc.) but that does not seem too practical and might not be repeatable to other properties that I might be interested in binding later on. And after a couple of days of searching online when I find the time, I am still no closer to finding other ideas. So, are there any proper solutions to my issue short of diving into the code of the control itself? (Supposedly, the license will give me access to the source code, but I would rather avoid direct modifications to that).

I have avoided specifying the QWhale editor in the question title and tags as I have felt that what I am looking for does not depend on this specific control. Please correct me if I am wrong.

I cannot provide my testing code at the moment due to being on a different computer, but if you think it is necessary just leave me a note and I will add it.


Update: Here is the code, because I am not sure that I managed to clearly describe my issue.

class ExtenEdit : TextEditor
{
    public static DependencyProperty TextProperty =
        DependencyProperty.Register("Text", typeof(string), typeof(ExtenEdit),
            new PropertyMetadata((obj, args) =>
            {
                TextEditor target = (TextEditor)obj;
                target.DocumentText = (string)args.NewValue;
            })
        );

    public new string Text
    {
        get { return base.Text; }
        set
        {
            if (base.Text != value)
            {
                base.Text = value;
            }
        }
    }
}

That works when I modify the value in the ViewModel, but when typing in the editor my property gets bypassed and the base property is called directly. It works if I add something like this:

protected override void OnKeyUp(System.Windows.Input.KeyEventArgs e)
{
    SetCurrentValue(TextProperty, base.Text);
    base.OnKeyUp(e);
}

But as I said above, I can not consider that as a valid "clean" solution.

And here are the related parts in my XAML (you can notice that I am also playing with AvalonDock):

Namespaces:

<Window x:Class="AvalonDockQWhale.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:avalonDock="http://avalondock.codeplex.com"
    xmlns:converter="clr-namespace:AvalonDockQWhale.Converter"
    xmlns:pane="clr-namespace:AvalonDockQWhale.View.Pane"
    xmlns:editor="clr-namespace:QWhale.Editor.Wpf;assembly=QWhale.Editor.Wpf"
    xmlns:control="clr-namespace:AvalonDockQWhale.Control"
    xmlns:controlHelper="clr-namespace:AvalonDockQWhale.ControlHelper"
    x:Name="mainWindow"
    Title="MainWindow" Height="600" Width="800">

And the binding for my original attempt:

<control:ExtenEdit Text="{Binding Path=ScriptText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />

The bindings that I have tried for DHN's solution:

<editor:TextEditor controlHelper:AttachedProperties.Text="{Binding ScriptText}" />

and

<editor:TextEditor controlHelper:AttachedProperties.Text="{Binding Path=ScriptText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />

Final details: I have accepted the answer given by DHN, even though it does not work in my specific case, as it seems to be a decent solution for similar issues.

Community
  • 1
  • 1
Amarus
  • 137
  • 10
  • the issue with using overriden Text property, the dependency-property declaration need to define/specific the type-owner, i believe you should set this to your customized control. – Kelmen Apr 24 '13 at 03:19

1 Answers1

1

Here is something I wrote just yesterday to help another SOler. But it may also fit your needs. I "extended" a Window via DependencyProperty, so that its DialogResult property is bindable.

Begin

public class AttachedProperties : DependencyObject //adds a bindable DialogResult to window
{
    public static readonly DependencyProperty DialogResultProperty = 
        DependencyProperty.RegisterAttached("DialogResult", typeof(bool?), typeof(AttachedProperties), 
        new PropertyMetaData(default(bool?), OnDialogResultChanged));

    public bool? DialogResult
    {
        get { return (bool?)GetValue(DialogResultProperty); }
        set { SetValue(DialogResultProperty, value); }
    }

    private static void OnDialogResultChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var window = d as Window;
        if (window == null)
            return;

        window.DialogResult = (bool?)e.NewValue;
    }
}

Now you can bind DialogResult to a VM and set its value of a property. The Window will close, when the value is set.

<!-- Assuming that the VM is bound to the DataContext and the bound VM has a property DialogResult -->
<Window someNs:AttachedProperties.DialogResult={Binding DialogResult} />

End

IMHO it's a good way to provide bindability, if you only need it for one particular property. If there are more properties which must be improved that way, then I would extend or wrap the class.

Edit - This is an abstract of what's running in our production environment

<Window x:Class="AC.Frontend.Controls.DialogControl.Dialog"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:DialogControl="clr-namespace:AC.Frontend.Controls.DialogControl" 
        xmlns:hlp="clr-namespace:AC.Frontend.Helper"
        MinHeight="150" MinWidth="300" ResizeMode="NoResize" SizeToContent="WidthAndHeight"
        WindowStartupLocation="CenterScreen" Title="{Binding Title}"
        hlp:AttachedProperties.DialogResult="{Binding DialogResult}" WindowStyle="ToolWindow" ShowInTaskbar="True"
        Language="{Binding UiCulture, Source={StaticResource Strings}}">
        <!-- A lot more stuff here -->
</Window>

As you can see, I'm declaring the namespace xmlns:hlp="clr-namespace:AC.Frontend.Helper" first and afterwards the binding hlp:AttachedProperties.DialogResult="{Binding DialogResult}".

The AttachedProperty looks like this. It's not the same I posted before, but IMHO it shouldn't make any difference.

public class AttachedProperties
{
    #region DialogResult

    public static readonly DependencyProperty DialogResultProperty =
        DependencyProperty.RegisterAttached("DialogResult", typeof (bool?), typeof (AttachedProperties), new PropertyMetadata(default(bool?), OnDialogResultChanged));

    private static void OnDialogResultChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var wnd = d as Window;
        if (wnd == null)
            return;

        wnd.DialogResult = (bool?) e.NewValue;
    }

    public static bool? GetDialogResult(DependencyObject dp)
    {
        if (dp == null) throw new ArgumentNullException("dp");

        return (bool?)dp.GetValue(DialogResultProperty);
    }

    public static void SetDialogResult(DependencyObject dp, object value)
    {
        if (dp == null) throw new ArgumentNullException("dp");

        dp.SetValue(DialogResultProperty, value);
    }

    #endregion
}
Community
  • 1
  • 1
DHN
  • 4,807
  • 3
  • 31
  • 45
  • That does sound to be what I'm looking for but I can't seem to put it in place. I've followed your example to create an `AttachedProperties` class, added the appropriate namespaces but the following XAML: `` Gives me the following compile error: `The attached property 'AttachedProperties.Text' is not defined on 'TextEditor' or one of its base classes.` – Amarus Apr 24 '13 at 08:04
  • I've just realised that I forgot to thank you for the help! So yeah, I really appreciate you taking the time to clarify this for me. The changes in `AttachedProperties` do make a difference as it does compile now. I would say it's the `static` getter/setter that made the difference. Except that binding is still only working one way for me, VM to editor and not the other way around. Nothing in the `AttachedProperties` gets called when I type in the editor. – Amarus Apr 24 '13 at 08:26
  • Sure, I've edited my question to include everything. It's probably something very stupid that I've missed due to being very new to all of that stuff. – Amarus Apr 24 '13 at 08:37
  • Ah damn, the problem is, that the control doesn't work with the `AttachedProperty`, it's still writing the text into its own `Text` property. It's now clear to me, wherefrom should the editor be aware of it? Hmm, I'm afraid, you'll have to add an event handler in code behind, so you don't need the whole `AttachedProperty` thing at all. -.- I'm sorry. But that also means your approach won't work either you posted before under certain circumstances. – DHN Apr 24 '13 at 09:05
  • Ah, I see. Well this confirms my suspicions, it can't be done in this case without going in the control's code. Still, thanks for introducing me to attached properties! – Amarus Apr 24 '13 at 09:18