3
\$\begingroup\$

I need to implement something like "attached properties" from WPF that targets WinForms.

What I came up with seems to work. Can you find any issues with it? The helper class and example are shown below.

  • Not thread-safe since all access should be performed from the UI thread.
  • Attach a value (SetAttachedValue) or getter/+setter (AttachProperty).
  • All properties are un-attached when the target component is disposed.
  • The primary use-case is that an IExtenderProvider will attach properties to various controls/components of a form and then other code will be able to access those attached properties without having to have a reference to the original IExtenderProvider.

Note:

  • This is experimental code that I'm doing to port Prism to WinForms. There are very few changes needed to do the port, but unfortunately to really complete the job I just need something like attached properties.

  • The code lives at github.com/misct/prism-winforms where I have already posted a slightly improved implementation and where I will soon post a much better implementation.

ExtenderHelper.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;

namespace TryAttachedProps
{
    public static class ExtenderHelper
    {
        #region Property Data Classes

        class AttachedProperty
        {
            public Func<Component, object> Getter;
            public Action<Component, object> Setter;
            public object Value;
        }

        class AttachedPropertyMap : Dictionary<string, AttachedProperty> { }

        #endregion

        static Dictionary<Component, AttachedPropertyMap> _attachedProperties =
            new Dictionary<Component, AttachedPropertyMap>();

        public static void AttachProperty(this Component component, string name, Func<Component, object> getter)
        {
            AttachProperty(component, name, getter, ReadOnlySetter);
        }

        public static void AttachProperty(this Component component, string name, Func<Component, object> getter, Action<Component, object> setter)
        {
            AttachedPropertyMap props;
            if (!_attachedProperties.TryGetValue(component, out props))
            {
                props = new AttachedPropertyMap();
                props.Add(name, new AttachedProperty { Getter = getter, Setter = setter });
                AttachPropertyMap(component, props);
                return;
            }
            AttachedProperty prop;
            if (!props.TryGetValue(name, out prop))
            {
                props.Add(name, new AttachedProperty { Getter = getter, Setter = setter });
                return;
            }
            prop.Getter = getter;
            prop.Setter = setter;
            prop.Value = null;
        }

        static void AttachPropertyMap(Component component, AttachedPropertyMap props)
        {
            if (component == null)
                throw new ArgumentNullException("component");

            _attachedProperties.Add(component, props);

            component.Disposed += component_Disposed;
        }

        static void component_Disposed(object sender, EventArgs e)
        {
            var component = sender as Component;
            component.Disposed -= component_Disposed;
            if (component != null)
                _attachedProperties.Remove(component);

            // CONSIDER: Should we dispose any IDisposable properties attached to the component or do other cleanup? (I think no. See updated Questions section edit in post.)
        }

        public static object GetAttachedValue(this Component component, string name)
        {
            AttachedPropertyMap props;
            if (_attachedProperties.TryGetValue(component, out props))
            {
                AttachedProperty prop;
                if (props.TryGetValue(name, out prop))
                {
                    var getter = prop.Getter;
                    if (getter != null)
                        return getter(component);

                    return prop.Value;
                }
            }
            return null;
        }

        static void ReadOnlySetter(Component component, object value)
        {
            throw new InvalidOperationException("The property is read-only.");
        }

        public static void SetAttachedValue(this Component component, string name, object value)
        {
            AttachedPropertyMap props;
            if (!_attachedProperties.TryGetValue(component, out props))
            {
                props = new AttachedPropertyMap();
                props.Add(name, new AttachedProperty { Value = value });
                AttachPropertyMap(component, props);
                return;
            }
            AttachedProperty prop;
            if (!props.TryGetValue(name, out prop))
            {
                props.Add(name, new AttachedProperty { Value = value });
                return;
            }
            var setter = prop.Setter;
            if (setter != null)
                setter(component, value);
            else
                prop.Value = value;
        }
    }
}

Form1.cs

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();

        // Uncomment one of the next three lines to try different tactics.
        this.SetAttachedValue("Test", "Hello");
        //this.AttachProperty("Test", c => _test);
        //this.AttachProperty("Test", c => _test, SetTestValue);

    }

    string _test = "Hello";

    void SetTestValue(Component component, object value)
    {
        _test = value as string;
    }

    private void button1_Click(object sender, EventArgs e)
    {
        var value = this.GetAttachedValue("Test") as string;

        this.SetAttachedValue("Test", value + " again!");

        button1.Text = value;

        button1.AutoSize = true;
    }
}
\$\endgroup\$
8
  • \$\begingroup\$ Whats the point if you don't have the databinding and change notification? \$\endgroup\$
    – Euphoric
    Feb 22 '14 at 20:36
  • \$\begingroup\$ I believe you can do data binding with IExtenderProvider provided properties and I think I could add change notification to this. Even if I can't do the data binding, it's alright with me because I want to attach something like Microsoft Prism's RegionManager (but my own implementation) to controls/forms/etc. \$\endgroup\$
    – wizlb
    Feb 22 '14 at 20:37
  • \$\begingroup\$ I guess my other question would be - what else would you do if you wanted to associate one or more values or getter/setters with a control/form/etc...but be able to access them from any code that has a reference to the control? \$\endgroup\$
    – wizlb
    Feb 22 '14 at 20:39
  • \$\begingroup\$ How is this any different than having a public List<Object> AttachedValues on the form? \$\endgroup\$ Feb 22 '14 at 20:44
  • 1
    \$\begingroup\$ You can attach values to classes you didn't create by extending/inheriting from them. \$\endgroup\$ Feb 22 '14 at 21:29
4
\$\begingroup\$

WinForms is not WPF.

Sad, boring truth. The correct way of extending WinForms controls is, as was mentioned, through inheritance.

What you've got here is a set of extension methods in a dual-purpose static class that's asking for trouble in the sense that it's also a state-holding bag of static objects that have "attached properties" - I think this code is abusing extension methods and static classes, be it only because the static class itself has two non-static "child" classes.

That said...

    class AttachedProperty
    {
        public Func<Component, object> Getter;
        public Action<Component, object> Setter;
        public object Value;
    }

You're exposing object, which incurs boxing of value types. And then you're storing state in a dictionary of dictionaries - which is a code smell IMO (begging for a type/class here).

I think the static class with the extension methods should be called ComponentExtensions, but its dual purpose defeats that. I think the class is breaking SRP.

I can't think of other ways to do this though... because I don't think I get the point. If you want to do WPF, drop WinForms and do WPF, don't try to turn a Corolla into an Audi!

\$\endgroup\$
8
  • \$\begingroup\$ Have you looked at the WPF implementation of DependencyObject/Property? I've been using it as a reference for a newer/better implementation of this and it's really not much different than what I'm doing here. \$\endgroup\$ Feb 27 '14 at 0:13
  • 1
    \$\begingroup\$ The thing is, WPF is so much more than DependencyProperties. Perhaps you can succeed in reinventing that wheel, but IMO it's like mounting a silver-plated wheel on an old rusted car - it can work, but one can tell that something just isn't right. A nice car is much more than shiny wheels. \$\endgroup\$ Feb 27 '14 at 0:18
  • 1
    \$\begingroup\$ Lastly, I know WPF and Silverlight very well but it's certainly not the best choice for many apps, particularly quick little IT operations apps that I need. However, I like to use Prism and MVVM so I'm doing this. \$\endgroup\$ Feb 27 '14 at 0:18
  • 1
    \$\begingroup\$ Well I'm sorry, but you don't know what you're talking about. You haven't seen what I can do with MVVM and WinForms. I can build apps very, very quickly this way, much quicker than WPF and honestly - they work better. No wierd focus lines and font problems. So please, stop patronizing me. \$\endgroup\$ Feb 27 '14 at 0:19
  • 1
    \$\begingroup\$ Well I'm sorry, it's just that I already argued this with someone in the comments above and I thought I made it clear that I don't care to discuss that aspect of the work. I'm not the first person to use MVVM with WinForms and I'm not even the first person to port Prism to WinForms. With the work I'm doing, code will be very easily portable between WinForms and WPF if I choose to port it later. Thanks for your input about the data structures. I will clean that up and come back here when I finished the port some more. Thanks. \$\endgroup\$ Feb 27 '14 at 0:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.