Fields or Properties after Clone?

Jan 6, 2011 at 4:14 PM
Edited Jan 6, 2011 at 7:50 PM

When using the property dialog, would it be problematic to copy fields instead of properties?

Let me give you little background on why I want to do this: I have an XML file that defines a user interface layout. The layout is organized into a hierarchy such that there is a top level node called Plants which may contain some number of Plant nodes.  Each Plant node may contain some number of Group nodes which may contain Control and Indicator nodes, as well as other sub-Group nodes. When the application starts up, the XML is deserialized into string FIELDS of an object hierarchy. I find it is better to use only string fields when deserializing so as to minimize deserialization errors. I then use properties to make data conversions in and out of the string fields.

Groups, Controls and Indicators all have properties that define there look; color; size; position; etc. When a property is accessed, if it finds that the corresponding field is null, the code will traverse up the hierarchy (Group to Plant to Plants) looking for some non-null entry and use that. So with that, you can, for example, have a command_width node property in the XML at the Plants level and all command button objects in all groups in all plants will have the same width.

Then I started working on my own property editor, using the the IEditableObject interface. Instead of cloning the object through properties in BeginEdit, I only copied fields because if I used GetProperties p and p.SetValue, it would then mess up the property inheritance mechanism when ever a Control or Indicator was edited. If a user edits a Control for example, and changes the width, the code should only update the width property for the control, not every property which would then supersede inherited properties.

Then I found your great software, and see that it's cloning through properties. For my application, as I've designed it, this is a problem. Unless there is some feature I'm not aware of, at this point it looks like my options are:

1. Make some change to the WPF PropertyEditor (fun)

2. Go back to using my own clunky property editor (yuck)

3. re-write my application to play nice with your WPF PropertyEditor (yuckier)

So, questions: Is there some feature I can take advantage of to make the pain go away? If not, could you recommend the best way to change PropertyEditor for option 1?

 

Thanks,

Bill

Jan 6, 2011 at 7:54 PM

I made some edits to my original post.

I also made changed MemberwiseClone and CommitChanges in PropertyDialog.xaml.cs:

        public bool CloneFields { get; set; }

private object MemberwiseClone(object src)
{
Type t = src.GetType();
object clone = Activator.CreateInstance(t);

if (CloneFields) {
foreach (
FieldInfo fi in t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) {
fi.SetValue(clone, fi.GetValue(src));
}
} else {
foreach (
PropertyInfo pi in t.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
.Where(pi => pi.CanWrite && pi.GetIndexParameters().Length == 0)) {
pi.SetValue(clone, pi.GetValue(src, null), null);
}
}
return clone;
}

private void CommitChanges()
{
// copy changes from cloned object (stored in PropertyEditor.DataContext) // to the original object (stored in DataContext) object clone = PropertyControl.DataContext;
if (clone==null)
return;

if (CloneFields) {
foreach (FieldInfo fi in clone.GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) {
object newValue = fi.GetValue(clone);
object oldValue = fi.GetValue(DataContext);

if (oldValue == null && newValue == null)
continue;

fi.SetValue(DataContext, newValue);
}
} else {
foreach (
PropertyInfo pi in clone.GetType().GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .Where(pi => pi.CanWrite && pi.GetIndexParameters().Length == 0)) { object newValue = pi.GetValue(clone, null);
object oldValue = pi.GetValue(DataContext, null);

if (oldValue==null && newValue==null)
continue;

if (oldValue!=null && !oldValue.Equals(newValue))
pi.SetValue(DataContext, newValue, null);
}
}
}


I'm still working out a little issue with a null reference the first time MemberwiseClone is called, but it mostly of seems to work.

What do you think? 

Thanks,

Bill

Jan 6, 2011 at 10:40 PM

There are, at this point, at least two problems with my solution:

First, in CommitChanges, calling SetValue on the FieldInfo won't trigger a call to NotifyPropertyChanging and NotifyPropertyChanged. The end result there is the change is not visualized. Second, in PropertyEditor.cs a call to UpdateContent is made at one point when the PropertyDialog is first opened and there is a line model = CreatePropertyModel(SelectedObject, false); which returns null so the subsequent reference of model.Count throws an exception.

I think maybe the way to go here would be to use an Interface for BeginEdit, EndEdit, and CacelEdit.

Maybe something like:

private void BeginEdit() {

    IEditableObject EditableDataContext = DataContext as IEditableObject;

    if(EditableDataContext != null) {
        EditableDataContext.BeginEdit()
    } else {
        object clone = MemberwiseClone(DataContext);
        PropertyControl.DataContext = clone;
    }
}

In my pre-property editor solution, when I was rolling my own, I would call NotifyPropertyChanged for every property, regardless of which one was changed. That's a clunky solution so some way to kwno which property has changed would be good too.

Any ideas?

 

Thanks,

Bill

Coordinator
Jan 7, 2011 at 7:21 AM

hi Bill! I am currently on vacation, but I will look into your changes later! here are some quick comments:

The PropertyEditor and PropertyDialog should not support editing of fields, only properties. 

The PropertyDialog class is at an early stage, I am thankful for all feedback that can make it better!

Could you use a custom PropertyDescriptorProvider for your model to provide properties the PropertyEditor can use?

Jan 7, 2011 at 2:36 PM

Since binding to fields is not possible (afaik) I'm still editing properties. The issue for me is what field the property will end up setting; the local object of the DataContext or the parent Group or Plant object for which some properties are inherited.

I'll take a look at the PropertyDescriptorProvider.

Enjoy the rest of your vacation.

Bill

Jan 7, 2011 at 9:20 PM

Looks like some sort of custom PropertyDescriptor would work, but I don't have any experience with this class so from my vantage point, it's not clear exactly how much coding it might require. I think I would need to add something like a dynamic IsInherited attribute and some means to know from which parent object the property is being inherited. Then, I think I'd need to change the setter on every property so as to get the value back to the right parent, if inherited.

Here's an example of a typical property from a Command object:

 

        [XmlIgnore]
        public System.Nullable<double> CommandWidth {
            get {
                double command_width;

                if (!double.TryParse(this.command_width, out command_width)) {
                    if (this.Parent != null) {
                        RMC_ACT_Group group = this.Parent as RMC_ACT_Group;
                        return group.CommandWidth;
                    } else return null;
                } else return command_width;
            }
            set {
                NotifyPropertyChanging();
                this.command_width = value.ToString();
                NotifyPropertyChanged("CommandWidth");
            }
        }


You can imagine that the RMC_ACT_Group class has a similar CommandWidth property that, if not set, checks his parent. Obviously, I'd like to find a solution that fits the existing design.

At the moment, I'm pursuing the IEditableObject DataContext solution but there are some issues and clunkiness. More on that later.

 

Thanks,

Bill

Jan 7, 2011 at 10:37 PM
Edited Jan 10, 2011 at 2:13 PM

Looks like I mostly have a solution using the IEditableObject interface. After I thought of it, I looked at the commented code in BeginEdit more closely and noticed where this solution appears to have been in seed form already, so that made me feel like I was on the right track.

A couple of side effects of this solution are 1) that there isn't a way (that I can think of) to swap in a new DataContext when BeginEdit is called. So consequently, I make a backup of the objects fields and then restore them if CancelEdit. And 2), since I do that, making changes to properties in the dialog immediately makes the change as soon as you cursor out of the editor. However, I actually sort of like that but some sort of undo without having to cancel would be cool (maybe I'll add an undo button to the dialog window). and 3) (this is what I think is the clunky part) as you can see in CancelEdit, I need to loop through every property and call NotifyPropertyChanging/NotifyPropertyChanged (sometimes through the Cascader). It would be nice to know which property(s) changed and just do those, however that then brings the complexity (I think) of needing some way to know which field a particular property is trying to set. Anyway, it seems fast enough as it is. I also don't like that this depends on the name of the property to determine which cascader to call, but I'll just call it a hack until I find something better.

I also found some problems in PropertyEditor.cs - UpdateContent(). It seemed to be getting called before a SelectedObject had been set, but I didn't go too far down the rabbit hole and only shored up UpdateContent().

Let me know what your think.

 

Thanks,

Bill

Here's the DataContext's IEditableObject interface methods:

 

        void IEditableObject.BeginEdit() {
            if (!inTxn) {
                backupData = new RMC_ACT_Group();
                foreach (FieldInfo f in this.GetType().GetFields()) {
                    f.SetValue(backupData, f.GetValue(this));
                }
                inTxn = true;
            }
        }

        //HACK Find some way to do this without depending on the property name
        void IEditableObject.CancelEdit() {
            if (inTxn) {

                PropertyInfo[] propertyInfo = this.GetType().GetProperties();

                foreach (PropertyInfo p in propertyInfo) {
                    if (p.Name.Contains("Group"))
                        this.CascadeNotifyPropertyChanging(typeof(RMC_ACT_Group));
                    else if(p.Name.Contains("Command"))
                        this.CascadeNotifyPropertyChanging(typeof(RMC_ACT_Command));
                    else if (p.Name.Contains("Indicator"))
                        this.CascadeNotifyPropertyChanging(typeof(RMC_ACT_Indicator));
                    else
                        NotifyPropertyChanging();
                }

                foreach (FieldInfo f in this.GetType().GetFields()) {
                    f.SetValue(this, f.GetValue(backupData));
                }

                foreach (PropertyInfo p in propertyInfo) {
                    if (p.Name.Contains("Group"))
                        this.CascadeNotifyPropertyChanged(typeof(RMC_ACT_Group), p.Name);
                    else if (p.Name.Contains("Command"))
                        this.CascadeNotifyPropertyChanged(typeof(RMC_ACT_Command), p.Name);
                    else if (p.Name.Contains("Indicator"))
                        this.CascadeNotifyPropertyChanged(typeof(RMC_ACT_Indicator), p.Name);
                    else
                        NotifyPropertyChanged(p.Name);
                }

                backupData = null;
                inTxn = false;
            }
        }

        void IEditableObject.EndEdit() {
            if (inTxn) {
                backupData = null;
                inTxn = false;
            }
        }

Changes to PropertyEditor.cs - UpdateContent():

 

 

        public void UpdateContent()
        {
            if (!IsLoaded)
            {
                return;
            }

            if (tabControl == null)
            {
                throw new InvalidOperationException(PART_TABS + " cannot be found in the PropertyEditor template.");
            }

            if (contentControl == null)
            {
                throw new InvalidOperationException(PART_PAGE + " cannot be found in the PropertyEditor template.");
            }

            ClearModel();

            // Get the property model (tabs, categories and properties)
            if (SelectedObjects != null) {
                model = CreatePropertyModel(SelectedObjects, true);
            } else if (SelectedObject != null) {
                model = CreatePropertyModel(SelectedObject, false);
            } else {
                return;
            }


            if (ShowTabs)
            {
                tabControl.ItemsSource = model;
                if (tabControl.Items.Count > 0)
                {
                    tabControl.SelectedIndex = 0;
                }

                tabControl.Visibility = Visibility.Visible;
                contentControl.Visibility = Visibility.Collapsed;
            }
            else
            {
                var tab = model.Count > 0 ? model[0] : null;
                contentControl.Content = tab;
                tabControl.Visibility = Visibility.Collapsed;
                contentControl.Visibility = Visibility.Visible;
            }

            UpdatePropertyStates(SelectedObject);
            UpdateErrorInfo();
        }

 

Coordinator
Jan 8, 2011 at 7:34 AM

thanks, I will check this when I am back to my dev machine.

Jan 14, 2011 at 7:54 PM

So what do you think? If we can put this change in it would help. I'd like to grab the latest.

 

Thanks,

Bill

Coordinator
Jan 15, 2011 at 8:55 AM

hi Bill, 

I just fixed the bug when SelectedObject=null and you don't use tabs, I think it is almost the same as you did in UpdateContent (but I wanted to clear the content of the contentControl, not just return). Thanks for pointing this out!

Can you please make a small&complete class demonstrating an object that you want to edit with the PropertyDialog, it's a bit difficult to get it from the code fractions!

Jan 17, 2011 at 3:24 PM

OK, hear ya go. Small was fighting Complete but this should give you the general idea. I think you're probably smart enough and experienced enough to fill in the missing pieces.

BTW, what TZ are you in? I'm GMT-6.

Thanks,

Bill



private
RMC_ACT_Plants _plants; public class RMC_ACT_Plants { public RMC_ACT_Plant[] Plants; public void CascadeNotifyPropertyChanging(Type RMC_ACT_Type) { foreach (RMC_ACT_Plant plant in this.Plants) { plant.CascadeNotifyPropertyChanging(RMC_ACT_Type); } } public void CascadeNotifyPropertyChanged(Type RMC_ACT_Type, string propertyName) { foreach (RMC_ACT_Plant plant in this.Plants) { plant.CascadeNotifyPropertyChanged(RMC_ACT_Type, propertyName); } } public string group_font_size; [Category("Group")] public System.Nullable<double> GroupFontSize { get { double group_font_size; if (!double.TryParse(this.group_font_size, out group_font_size)) return null; else return group_font_size; } set { this.CascadeNotifyPropertyChanging(typeof(RMC_ACT_Group)); this.group_font_size = value.ToString(); this.CascadeNotifyPropertyChanged(typeof(RMC_ACT_Group), "GroupFontSize"); } } public string command_font_size; [Category("Command")] public System.Nullable<double> CommandFontSize { get { double command_font_size; if (!double.TryParse(this.command_font_size, out command_font_size)) return null; else return command_font_size; } set { this.CascadeNotifyPropertyChanging(typeof(RMC_ACT_Command)); this.command_font_size = value.ToString(); this.CascadeNotifyPropertyChanged(typeof(RMC_ACT_Command), "CommandFontSize"); } } } public class RMC_ACT_Plant { public RMC_ACT_Group[] Groups; public void CascadeNotifyPropertyChanging(Type RMC_ACT_Type) { foreach (RMC_ACT_Group group in this.Groups) group.CascadeNotifyPropertyChanging(RMC_ACT_Type); } public void CascadeNotifyPropertyChanged(Type RMC_ACT_Type, string propertyName) { foreach (RMC_ACT_Group group in this.Groups) { group.CascadeNotifyPropertyChanged(RMC_ACT_Type, propertyName); } } public string rows; public int Rows { get { int rows = 0; int.TryParse(this.rows, out rows); return rows; } set { NotifyPropertyChanging(); this.rows = value.ToString(); NotifyPropertyChanged("Rows"); } } public string group_font_size; [Category("Group")] public System.Nullable<double> GroupFontSize { get { double group_font_size; if (!double.TryParse(this.group_font_size, out group_font_size)) { if (this.Parent != null) { RMC_ACT_Plants plants = this.Parent as RMC_ACT_Plants; return plants.GroupFontSize; } else return null; } else return group_font_size; } set { this.CascadeNotifyPropertyChanging(typeof(RMC_ACT_Group)); this.group_font_size = value.ToString(); this.CascadeNotifyPropertyChanged(typeof(RMC_ACT_Group), "GroupFontSize"); } } public string command_font_size; [Category("Command")] public System.Nullable<double> CommandFontSize { get { double command_font_size; if (!double.TryParse(this.command_font_size, out command_font_size)) { if (this.Parent != null) { RMC_ACT_Plants plants = this.Parent as RMC_ACT_Plants; return plants.CommandFontSize; } else return null; } else return command_font_size; } set { this.CascadeNotifyPropertyChanging(typeof(RMC_ACT_Command)); this.command_font_size = value.ToString(); this.CascadeNotifyPropertyChanged(typeof(RMC_ACT_Command), "CommandFontSize"); } } } public class RMC_ACT_Group { public RMC_ACT_Indicator[] Indicators; public RMC_ACT_Command[] Commands; public RMC_ACT_Group[] Groups; public void CascadeNotifyPropertyChanging(Type RMC_ACT_Type) { if (RMC_ACT_Type == typeof(RMC_ACT_Group)) { this.NotifyPropertyChanging(); } else if (RMC_ACT_Type == typeof(RMC_ACT_Command)) { if (this.Commands != null) { foreach (RMC_ACT_Command command in this.Commands) command.NotifyPropertyChanging(); } } else if (RMC_ACT_Type == typeof(RMC_ACT_Indicator)) { if (this.Indicators != null) { foreach (RMC_ACT_Indicator indicator in this.Indicators) indicator.NotifyPropertyChanging(); } } if (this.Groups != null) { foreach (RMC_ACT_Group subgroup in this.Groups) subgroup.CascadeNotifyPropertyChanging(RMC_ACT_Type); } } public void CascadeNotifyPropertyChanged(Type RMC_ACT_Type, string propertyName) { if (RMC_ACT_Type == typeof(RMC_ACT_Group)) { this.NotifyPropertyChanged(propertyName); } else if (RMC_ACT_Type == typeof(RMC_ACT_Command)) { if (this.Commands != null) { foreach (RMC_ACT_Command command in this.Commands) command.NotifyPropertyChanged(propertyName); } } else if (RMC_ACT_Type == typeof(RMC_ACT_Indicator)) { if (this.Indicators != null) { foreach (RMC_ACT_Indicator indicator in this.Indicators) indicator.NotifyPropertyChanged(propertyName); } } if (this.Groups != null) { foreach (RMC_ACT_Group subgroup in this.Groups) { subgroup.CascadeNotifyPropertyChanged(RMC_ACT_Type, propertyName); } } } public string rows; public int Rows { get { int rows = 0; int.TryParse(this.rows, out rows); return rows; } set { NotifyPropertyChanging(); this.rows = value.ToString(); NotifyPropertyChanged("Rows"); } } public string group_font_size; [Category("Group")] public System.Nullable<double> GroupFontSize { get { double group_font_size; if (!double.TryParse(this.group_font_size, out group_font_size)) { if (this.Parent != null) { if (this.Parent is RMC_ACT_Plant) { RMC_ACT_Plant plant = this.Parent as RMC_ACT_Plant; return plant.GroupFontSize; } else if (this.Parent is RMC_ACT_Group) { RMC_ACT_Group group = this.Parent as RMC_ACT_Group; return group.GroupFontSize; } else return null; } else return null; } else return group_font_size; } set { this.CascadeNotifyPropertyChanging(typeof(RMC_ACT_Group)); this.group_font_size = value.ToString(); this.CascadeNotifyPropertyChanged(typeof(RMC_ACT_Group), "GroupFontSize"); } } public string command_font_size; [Category("Commands")] public System.Nullable<double> CommandFontSize { get { double command_font_size; if (!double.TryParse(this.command_font_size, out command_font_size)) { if (this.Parent != null) { if (this.Parent is RMC_ACT_Plant) { RMC_ACT_Plant plant = this.Parent as RMC_ACT_Plant; return plant.CommandFontSize; } else if (this.Parent is RMC_ACT_Group) { RMC_ACT_Group group = this.Parent as RMC_ACT_Group; return group.CommandFontSize; } else return null; } else return null; } else return command_font_size; } set { this.CascadeNotifyPropertyChanging(typeof(RMC_ACT_Command)); this.command_font_size = value.ToString(); this.CascadeNotifyPropertyChanged(typeof(RMC_ACT_Command), "CommandFontSize"); } } } public class RMC_ACT_Command : INotifyPropertyChanged, INotifyPropertyChanging, IEditableObject { public int Row { get { int row = 0; int.TryParse(this.row, out row); return row; } set { NotifyPropertyChanging(); this.row = value.ToString(); NotifyPropertyChanged("Row"); } } public string command_font_size; public System.Nullable<double> CommandFontSize { get { double command_font_size; if (!double.TryParse(this.command_font_size, out command_font_size)) { if (this.Parent != null) { RMC_ACT_Group group = this.Parent as RMC_ACT_Group; return group.CommandFontSize; } else return null; } else return command_font_size; } set { NotifyPropertyChanging(); this.command_font_size = value.ToString(); NotifyPropertyChanged("CommandFontSize"); } } }

 

Jan 18, 2011 at 2:38 PM

Also, my changes in PropertyDialog.xaml.cs:

        private void BeginEdit()
        {
            IEditableObject EditableDataContext = DataContext as IEditableObject;

            if(EditableDataContext != null) {
                EditableDataContext.BeginEdit();
            } else {
                PropertyControl.DataContext = MemberwiseClone(DataContext);
            }
        }

        private void EndEdit()
        {
            IEditableObject EditableDataContext = DataContext as IEditableObject;

            if (EditableDataContext != null) {
                EditableDataContext.EndEdit();
            } else {
                CommitChanges();
            }
        }

        private void CancelEdit()
        {
            IEditableObject EditableDataContext = DataContext as IEditableObject;

            if (EditableDataContext != null) {
                EditableDataContext.CancelEdit();
            } 
        }


And what things look like in the DataContext object:
        void IEditableObject.BeginEdit() {
            if (!inTxn) {
                backupData = new RMC_ACT_Group();
                foreach (FieldInfo f in this.GetType().GetFields()) {
                    f.SetValue(backupData, f.GetValue(this));
                }
                inTxn = true;
            }
        }

        //HACK Find some way to do this without depending on the property name
        void IEditableObject.CancelEdit() {
            if (inTxn) {

                PropertyInfo[] propertyInfo = this.GetType().GetProperties();

                foreach (PropertyInfo p in propertyInfo) {
                    if (p.Name.Contains("Group"))
                        this.CascadeNotifyPropertyChanging(typeof(RMC_ACT_Group));
                    else if(p.Name.Contains("Command"))
                        this.CascadeNotifyPropertyChanging(typeof(RMC_ACT_Command));
                    else if (p.Name.Contains("Indicator"))
                        this.CascadeNotifyPropertyChanging(typeof(RMC_ACT_Indicator));
                    else
                        NotifyPropertyChanging();
                }

                foreach (FieldInfo f in this.GetType().GetFields()) {
                    f.SetValue(this, f.GetValue(backupData));
                }

                foreach (PropertyInfo p in propertyInfo) {
                    if (p.Name.Contains("Group"))
                        this.CascadeNotifyPropertyChanged(typeof(RMC_ACT_Group), p.Name);
                    else if (p.Name.Contains("Command"))
                        this.CascadeNotifyPropertyChanged(typeof(RMC_ACT_Command), p.Name);
                    else if (p.Name.Contains("Indicator"))
                        this.CascadeNotifyPropertyChanged(typeof(RMC_ACT_Indicator), p.Name);
                    else
                        NotifyPropertyChanged(p.Name);
                }

                backupData = null;
                inTxn = false;
            }
        }

        void IEditableObject.EndEdit() {
            if (inTxn) {
                backupData = null;
                inTxn = false;
            }
        }


Coordinator
Jan 19, 2011 at 8:02 PM

hi Bill, I just checked in the patch on Begin/End/CancelEdit on the PropertyDialog.

Jan 19, 2011 at 8:15 PM

OK. Thanks.