-1

In the last couple of weeks I've been learning PropertyGrid. I need to display the properties of a series of objects of various classes, but they are all derived from class Ctrl. For instance, there are:

  • Ctrl_BUTTON
  • Ctrl_SQLLISTVIEW
  • Ctrl_TEXTBOX
  • nine classes in all

The derived classes contain the additional properties that are not in the base class and only apply to some of the derived classes. Each property can either be of a specific type of an Expression (a class that allows the user to enter a string that will later evaluate to a canonical value). In the PropertyGrid, I've implemented a dropdown by writing class ExpressionPropertyEditor which inherits UITypeEditor. I've currently only implemented the Ctrl_TEXTBOX.ReadOnly property which can be a bool, or an Expression, and it provides a dropdown that looks like this before the user has entered an expression:

Boolean property without customised Expression

or something like this if they have:

Boolean property with customised Expression

When the user clicks on the Expression entry, an expression editor is opened. Currently the relevant parts of my Ctrl_TEXTBOX class look like this:

    public class Ctrl_TEXTBOX : Ctrl
    {
        private object _readOnly = false;

        [DescriptionAttribute("Whether the user can change the contents.")]
        [Editor(typeof(ExpressionPropertyEditor), typeof(UITypeEditor))]
        public object ReadOnly
        {
            get
            {
                return _readOnly;
            }
            set
            {
                try
                {
                    _readOnly = ExpressionHelper.BoolExp2Object(value);
                }
                catch (Exception ex)
                {
                    base.Globals.Errs.Raise(ex);
                    throw ex;
                }
            }
        }
    }

For reference ExpressionHelper contains this, simply:

   public static class ExpressionHelper
    {
        public static object BoolExp2Object(object oValue)
        {
            try
            {
                switch (Helper.GetClassNameFromObject(oValue).ToLower())
                {
                    case "expression": return oValue;
                    case "string":
                        switch (((string)oValue).ToLower())
                        {
                            case "true": return true;
                            case "false": return false;
                            default: throw new NotImplementedException();
                        }
                    case "boolean":
                    case "bool": return oValue;
                    default: throw new NotImplementedException();
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }
    }

My implementation of ExpressionPropertyEditor looks like this:

    public class ExpressionPropertyEditor : UITypeEditor
    {
        private IWindowsFormsEditorService _editorService;
        private ListBox _listBox;
        private Ctrl _ctrl;
        
        public ExpressionPropertyEditor()
        {
        }

        // Displays the UI for value selection.
        public override object EditValue(ITypeDescriptorContext context, System.IServiceProvider provider, object value)
        {
            // coded with help from: https://stackoverflow.com/questions/5171037/display-list-of-custom-objects-as-a-drop-down-in-the-propertiesgrid

            Ctrl oCtrl;
            Ctrl_TEXTBOX oTextBox;
            Expression oExp = null;
            frmExpressionEditor2 frm;
            bool bOk = false;

            _editorService = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));

            _listBox = new ListBox();
            _listBox.SelectionMode = SelectionMode.One;
            _listBox.SelectedValueChanged += EventHandler_ListBox_SelectedValueChanged;
            _listBox.Items.Add(true);
            _listBox.Items.Add(false);

            switch (Helper.GetClassNameFromObject(context.Instance))
            {
                case "Ctrl_TEXTBOX":

                    oTextBox = (Ctrl_TEXTBOX)context.Instance;
                    switch (Helper.GetClassNameFromObject(oTextBox.ReadOnly))
                    {
                        case "Boolean":
                        case "bool":
                            // cos we need a way to make an expression
                            oExp = new Expression(oTextBox.Globals, "BOOLEAN", enumExpressionSource.Expression, Consts.EXPRESSION_PROPERTY_NAME);
                            _listBox.Items.Add(oExp);
                            break;

                        case "Expression":
                            oExp = (Expression)oTextBox.ReadOnly;
                            _listBox.Items.Add(oExp);
                            break;

                        default:
                            // this shouldn't happen really; just wrap as an expression
                            oExp = new Expression(oTextBox.Globals, "BOOLEAN", enumExpressionSource.Expression, Consts.EXPRESSION_PROPERTY_NAME);
                            _listBox.Items.Add(oExp);
                            break;
                    }
                    break;
            }

            _ctrl = (Ctrl)context.Instance;
            _editorService.DropDownControl(_listBox); // this will return when EventHandler_ListBox_SelectedValueChanged calls editorService.CloseDropDown, like a modal dialog.

            if (_listBox.SelectedItem == null) // no selection, return the passed-in value as is
                return value;

            if (Helper.GetClassNameFromObject(_listBox.SelectedItem) == "Expression")
            {
                frm = new frmExpressionEditor2();
                if (!frm.EditExpression(_ctrl.Globals, _ctrl.Server, _ctrl.App, _ctrl.Frm, ref oExp, ref bOk)) throw new Exception("Could not open expression editor.");
            }

            return _listBox.SelectedItem;
        }

        private void EventHandler_ListBox_SelectedValueChanged(object sender, EventArgs e)
        {
            _editorService.CloseDropDown();
        }

        public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
        {
            return UITypeEditorEditStyle.DropDown;
        }

        public override bool IsDropDownResizable
        {
            get { return true; }
        }

    }

My question is: how do I generalize ExpressionPropertyEditor so that I can use it on any of the derived classes of Ctrl, for any of the boolean properties that I want to include Expressions with? Currently it is locked to Ctrl_TEXTBOX.ReadOnly. If this is not possible to do, I have to created dozens of classes containing identical logic with just the names changed - not good for code reuse.

Mark Roworth
  • 409
  • 2
  • 15
  • 1
    The combination context.Instance and context.PropertyDescriptor should be enough to know what property in what class is being edited. Next, a virtual method on Ctrl, overridden by the derived classes, could help perform class+property specic tasks. Moreover, you can declare the editor as a nested class of Ctrl to access private and protected members. – Rubidium 37 Apr 13 '21 at 09:28
  • You should answer people if you want them to continue to answer. – Simon Mourier Apr 13 '21 at 09:50
  • I'm very sorry Simon, I'll try and jump and skip to your time schedule. I'm currently looking at making some of the changes that Rubidium has kindly suggested and will feedback when I've got, uh, something useful to feedback. – Mark Roworth Apr 13 '21 at 10:04
  • @Rubidium 37, thanks v much. I now have a generic method that will work for any boolean value that is on a sub-class of Ctrl. Took me a while as I'm not very familiar with the Reflection methods which I've had to use to obtain values without having to tightly bind context.Instance to a specific type. I haven't yet nested it inside Ctrl (and may not really need to). I'll post the modified code below. Thanks again. – Mark Roworth Apr 13 '21 at 19:36

1 Answers1

-1

Thanks to @Rubidium 37's comments above, I've modified EditValue method so that it doesn't need to bind tightly to a sub-class of Ctrl and can use any property name. I've then used the PropertyInfo class in System.Reflection to obtain the current value of the property without having to hardcode the property name.

Just the updated EditValue method is posted.

       public override object EditValue(ITypeDescriptorContext context, System.IServiceProvider provider, object value)
        {
            // coded with help from: https://stackoverflow.com/questions/5171037/display-list-of-custom-objects-as-a-drop-down-in-the-propertiesgrid

            Ctrl oCtrl = null;
            Expression oExp = null;
            frmExpressionEditor2 frm = null;
            bool bOk = false;
            System.Reflection.PropertyInfo oPropInfo = null;
            string cPropertyName = "";
            object oCurrentValue = null;

            try
            {
                oCtrl = (Ctrl)context.Instance;
                _editorService = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));

                // setup a listbox with the possible values the boolean can take, true, false, and an expression
                _listBox = new ListBox();
                _listBox.SelectionMode = SelectionMode.One;
                _listBox.SelectedValueChanged += EventHandler_ListBox_SelectedValueChanged;
                //_listBox.DrawItem += EventHandler_ListBox_DrawItem; // do this if you have loads of time and want a pretty fx next to the Expression
                _listBox.Items.Add(true);
                _listBox.Items.Add(false);

                // we either want to show Consts.EXPRESSION_PROPERTY_NAME, or if the value of the property is currently an expression, show that instead
                cPropertyName = context.PropertyDescriptor.Name;
                oPropInfo = context.Instance.GetType().GetProperty(cPropertyName);
                oCurrentValue = oPropInfo.GetValue(context.Instance, null); // this returns the current value of the property

                switch (Helper.GetClassNameFromObject(oCurrentValue))
                {
                    case "Boolean":
                    case "bool": // just show the default expression string
                        oExp = new Expression(oCtrl.Globals, "BOOLEAN", enumExpressionSource.Expression, Consts.EXPRESSION_PROPERTY_NAME);
                        _listBox.Items.Add(oExp);
                        break;

                    case "Expression": // show the current value of the boolean expression 
                        oExp = (Expression)oCurrentValue;
                        _listBox.Items.Add(oExp);
                        break;

                    default: // this shouldn't happen, so reset to default expression string
                        oExp = new Expression(oCtrl.Globals, "BOOLEAN", enumExpressionSource.Expression, Consts.EXPRESSION_PROPERTY_NAME);
                        _listBox.Items.Add(oExp);
                        break;
                }

                // show the list
                _editorService.DropDownControl(_listBox); // this will return when EventHandler_ListBox_SelectedValueChanged calls editorService.CloseDropDown, like a modal dialog.

                // no selection, return the passed-in value as is
                if (_listBox.SelectedItem == null) return value; 

                // and if necessary, allow the user to edit the expression
                if (Helper.GetClassNameFromObject(_listBox.SelectedItem) == "Expression")
                {
                    frm = new frmExpressionEditor2();
                    if (!frm.EditExpression(oCtrl.Globals, oCtrl.Server, oCtrl.App, oCtrl.Frm, ref oExp, ref bOk)) throw new Exception("Could not open expression editor.");
                    return oExp;
                }

                return _listBox.SelectedItem;
            }
            catch (Exception ex)
            {
                oCtrl.Globals.Errs.Raise(ex);
                return null;
            }
        }
Mark Roworth
  • 409
  • 2
  • 15