views:

32

answers:

2

I'm enhancing an open source control to add some functionality that I need, and I'm getting hopelessly tangled up in the following problem:

The control is a rich textbox that supports HTML but not via a property; you have to do something like this:

var sHtml = "..."
ctrl.LoadHtml(sHtml)

and

var sHtml = ctrl.SaveHtml()

So far so good. But I want to set the HTML via data binding, so I made a dependency property called Html:

    public static readonly DependencyProperty HtmlProperty =
        DependencyProperty.Register(
            "Html",
            typeof(string),
            typeof(RichTextEditor),
            new PropertyMetadata(string.Empty, new PropertyChangedCallback(HtmlChangedCallback))
            );

    public string Html
    {
        get {return (string)GetValue(HtmlProperty);}

        set {SetValue(HtmlProperty, value);}
    }

    private static void HtmlChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        //get the control
        var rte = (RichTextEditor)d;

        //got here, so load the html
        rte.TextBox.LoadHtml((string)e.NewValue);
    }

This all works fine. The problem I'm having is that I can't figure out how to notify the property system when the contents of the control have changed. The control has a ContentChanged event, so I tried this:

    private void rtb_ContentChanged(object sender, RichTextBoxEventArgs e)
    {
        //tell the html prop that it changed
        SetValue(HtmlProperty, rtb.SaveHtml());
    }

But this then triggers the HtmlChangedCallback and the re-entrance caused problems. So then I tried using a re-entrance flag, but that got messy because the sequence of events is more complex than I would have expected, and around this point I figured I must be missing something, so I'm asking here. Please help! Thanks in advance.

BTW, the control doesn't support INotifyPropertyChanged, and implementing it is out of scope, because the control is big and I don't want to do that much work.

A: 

Why do you need to notify property system? Are you trying to update the source of the data binding? The control does not have to implement INotifyPropertyChanges; this interface has to be implemented on the class which provides the data for data binding.

What if you ignore Save/LoadHtml for now and just store HTML as a string? Does it work without problems?

Here is the example which works fine for me

class MyControl
{
    public int Value
    {
        get
        {
            return (int)GetValue(ValueProperty);
        }

        set
        {
            SetValue(ValueProperty, value);
            Count.Text = value.ToString();
        }
    }

    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.Register("Value",
            typeof(int),
            typeof(Vote),
            new PropertyMetadata(0, OnValueChanged));

    private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((MyControl)d).Value = (int)e.NewValue;
    }

Count is a text block control.

AlexEzh
Yes, I've got a ViewModel that should be updated when the Html in the View (that is, the rich textbox) changes. No, it doesn't work just storing the string, because there's still no way for the property system to know that the View has changed the string. My question is: what is the correct way to tell it when that has happened?
Joshua Frank
Did you specify TwoWay mode in the {Binding} statement? In order for things to work you need three things- model should implement INotifyPropertyChanges and invoke property change event- view should use dependency property (stored using Get/SetValue)- XAML should specify TwoWay mode
AlexEzh
I don't think those three steps are sufficient, since I'm doing all of them already. After thinking about it further, I think the missing piece is quite general: when you specify a binding that updates the source from the View (such as TwoWay), and the View doesn't implement INPC, how would it *ever* work? That is, there has to be a way for the View to notify that a property has changed, and I think it ought to be possible through the DependencyProperty, but I'm not so sure, because I can't find one. So maybe the View has to implement INPC if I want it to support updating the Source.
Joshua Frank
View knows to update binding because the view stores a property using SetValue. SetValue will perform all necessary updates.One additional thing which I do in my controls. I always call rte.Htlm = e.NewValue from the notification callback. When you say that things do not work, what do you mean? Do you get initial value of modelview set on the control?
AlexEzh
I get the notification callback just fine. But then the user types in the textbox and now the html has changed. How should the control notify the property system that this happened? Calling SetValue(HtmlProperty, html) doesn't work, because it triggers the notification callback.
Joshua Frank
I suspect that the problem happens because text control generates ContentChange event when you set content on it. Do you have an event which is triggered when user types (for SL text control you can use TextChanged)? I updated my original answer with a snippet which works for my app.
AlexEzh
Okay, this turned out to be right, although I'm a bit puzzled because I had tried this and it didn't work. When I looked more closely, I saw that the problem was not the one event, but that the Changed event actually fires multiple times, and I needed two flags to deal with this. However, I did learn while studying this that this line in your sample: Count.Text = value.ToString();is a very bad idea, because the property system often bypasses the setter code, and this will introduce bugs.
Joshua Frank
A: 

This is a classic little problem that is usually solved with a simple boolean flag.

private bool suppressHtmlChanged = false;

private static void HtmlChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{

    var rte = (RichTextEditor)d;

    if (!rte.suppressHtmlChanged)
        rte.TextBox.LoadHtml((string)e.NewValue);
}


private void rtb_ContentChanged(object sender, RichTextBoxEventArgs e)
{
    suppressHtmlChanged = true;
    SetValue(HtmlProperty, rtb.SaveHtml());
    suppressHtmlChanged = false;
}

These days such solutions seem so old fashioned don't they, often we can convince ourselves that such a "low-tech" solutions can't be right because they're not "elegant".

AnthonyWJones
Yes, this did the trick, although it turned out to be more complex in the specifics, because of the way this control fires events. Thanks.
Joshua Frank