0

I have a C# WPF PropertyGrid (Denys Vuika) that is a simple example to cement the logic for a larger implementation. My general approach is to create the grid and use a SelectedObject that is a Clone of the real Properties as the displayed/editable Properties. During the editing, the real Properties are untouched -- only the Clone is changed. The user is able to Cancel or OK the edit. Cancel will cause the Clone to reload the real Properties, essentially discarding all edits. OK will set the real Properties to be the same as the now-edited Clone. There will be an expanded set of actions, but for the simplest example, Cancel and OK are all that is needed.

I am able to successfully create, display and edit the grid. However, I am finding that the use of Real vs Clone seems to be causing issues with the values of the Properties being displayed/stored.

Example behaviour: a bool Property is true in Real and is changed to be false (the change, in theory, should be happening only in the Clone). Cancel should leave the Property as true, but it in fact changes it to false. There are other odd behaviours that are of similar nature, where the state of a Property seems to be incorrect after an edit action. I'm using bools in this example, again for simplicity, but the same behaviour occurs with other data types.

Here is the code for the PropertyGrid Window:

    internal class TESTPropertiesGrid : Window
    {
        private Button btnOK        = null;
        private Button btnCancel    = null;
        
        internal PropertyGrid      pgrProperties       = null;
        private  clsTestProperties pclPropertiesSource = null;
        private  clsTestProperties pclPropertiesClone  = null;
        
        // --- Constructor ---
        public TESTPropertiesGrid(clsTestProperties pSourceProperties)
        {
            pclPropertiesSource = pSourceProperties;
            
            Caption = "Test Editing Properties";
            Width   = 390;
            Height  = 320;
            
            Content = LoadXAML("TestPropertiesGrid.xaml");
            Closing += OnWindow_Closing;
        }
        
        private void OnWindow_Closing(object sender, CancelEventArgs e)
        {
            btnOK.Click         -= OnOK;
            btnCancel.Click     -= OnCancel;
            
            btnOK       = null;
            btnCancel   = null;
            
            pclPropertiesSource = null;
            pclPropertiesClone  = null;
            pgrProperties       = null;
        }
        
        private DependencyObject LoadXAML(string xamlFilename)
        {
            try
            {
                using (FileStream fs = new FileStream(xamlFilename, FileMode.Open))
                {
                    Page page = System.Windows.Markup.XamlReader.Load(fs) as Page;
                    if (page == null)
                        return null;
                    
                    DependencyObject pageContent = page.Content as DependencyObject;
                    
                    btnOK         = LogicalTreeHelper.FindLogicalNode(pageContent, "btnOK")     as Button;
                    btnCancel     = LogicalTreeHelper.FindLogicalNode(pageContent, "btnCancel") as Button;
                    pgrProperties = LogicalTreeHelper.FindLogicalNode(pageContent, "pgrDashboardProperties") as PropertyGrid;
                    
                    btnOK.Click += OnOK;
                    btnCancel.Click += OnCancel;
                    
                    ReloadProperties();
                    
                    return pageContent;
                }
            }
            catch (Exception erk)
            {
                // Output erk.ToString()
                return null;
            }
        }
        
        // --- Apply all changes to the active Properties, hide the editor ---
        private void OnOK(object sender, RoutedEventArgs e)
        {
            this.Hide();
            pclPropertiesSource = pclPropertiesClone;   // Save the Properties
            ReloadProperties();
        }
        
        // --- Ignore all changes, hide the editor ---
        private void OnCancel(object sender, RoutedEventArgs e)
        {
            this.Hide();
            ReloadProperties();
        }
        
        // --- Reload the Properties from the active Properties ---
        private void ReloadProperties()
        {
            // -- Clone the class so any changes are not pushed to the active Properties until OK'ed --
            pclPropertiesClone = null;
            pclPropertiesClone = pclPropertiesSource.Clone() as clsTestProperties;
            pgrProperties.Dispatcher.InvokeAsync(() =>
            {
                try
                {
                    pgrProperties.SelectedObject = null;
                    pgrProperties.SelectedObject = pclPropertiesClone;
                    pgrProperties.InvalidateVisual();
                }
                catch (Exception eek)
                {
                    // Output eek.ToString()
                }
            });
        }
    }

Here is the code for the Properties class. Note that it contains a Property at the top level and a nested Property. I am finding that the behaviour of the top level and the nested bools seems different.

    public class clsTestProperties : INotifyPropertyChanged, ICloneable
    {
        // -- Constructor to Set Defaults Properties --
        public clsTestProperties()
        {
            SelectedTestExpander = new TestExpander();
            SelectedTestExpander.NestedProperty = true;
            TestTopLevel = true;
        }
        
        // Event to flag that Property items have changed and the Property Grid should be regenerated
        public event PropertyChangedEventHandler PropertyChanged;
        
        // Event Handler to force an update to the Property Grid after changes to any Property
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            if (PropertyChanged != null)
                PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        
        // --- Create an identical copy of this structure ---
        public object Clone()
        {
            return this.MemberwiseClone();
        }
        
        [Display(GroupName = "Test", Order = 0, Name = "NON-Nested Property")]
        public bool TestTopLevel
        { get; set; }
        
        [Display(GroupName = "Test", Order = 1, Name = "Expandable Category")]
        public TestExpander SelectedTestExpander
        { get; set; }

        [TypeConverter(typeof(ExpandableObjectConverter))]
        public class TestExpander
        {
            public TestExpander()
            {
            }

            public override string ToString()
            {
                return string.Format("");
            }

            [Display(Order = 0, Name = "Nested Property")]
            public bool NestedProperty { get; set; }
        }
    }

And here is how I am instantiating the TESTPropertiesGrid window. This happens after a button click in the larger application, not included here.

        internal TESTPropertiesGrid wndPropertiesGrid = null;
        internal clsTestProperties  pclTestProperties = new clsTestProperties();
        
        private WindowInteropHelper wihWndProperties = null;
        
        // --- Display the Property Grid for editing ---
        private void EditProperties()
        {
            if (wndPropertiesGrid == null ||            // Never created
                wihWndProperties  == null ||            // Never created either
                wihWndProperties.Handle == IntPtr.Zero) // Window was Closed
            {
                wndPropertiesGrid = new TESTPropertiesGrid(this.pclTestProperties);
                wihWndProperties  = new WindowInteropHelper(wndPropertiesGrid);
            }
            
            wndPropertiesGrid.Dispatcher.InvokeAsync(new Action(() =>
            {
                try
                {
                    wndPropertiesGrid.Show();
                }
                catch (Exception erk)
                {
                    // Output erk.ToString()
                }
            }));
        }

Here is the XAML for the TESTPropertiesGrid window:

<Page   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:pg="http://schemas.denisvuyka.wordpress.com/wpfpropertygrid">
      
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="5" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="5" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="5" />
            <RowDefinition Height="*" />
            <RowDefinition Height="45" />
            <RowDefinition Height="5" />
        </Grid.RowDefinitions>
        
        <ScrollViewer Grid.Column="1" Grid.Row="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" VerticalScrollBarVisibility="Auto">
            <pg:PropertyGrid x:Name="pgrProperties"></pg:PropertyGrid>
        </ScrollViewer>
        
        <StackPanel Grid.Column="1" Grid.Row="2" HorizontalAlignment="Right" Orientation="Horizontal" VerticalAlignment="Bottom">
            <Button x:Name="btnOK"       Margin="5" Width="52" Content="OK"/>
            <Button x:Name="btnCancel"   Margin="5" Width="52" Content="Cancel"/>
        </StackPanel>
    </Grid>
</Page>

I have not included the actual Output methods as they are custom in the context of the larger application.

Hopefully, all of the above shows what I'm doing (and trying to do) and testable by anyone in the forum. My feeling is that I am seeing behaviour that is caused by "under-the-covers" actions that I am not familiar with and may not expect. There seems to be a difference in behaviour between top level and nested Properties that I do not understand. That is as may be.

In any case, I am looking to update the code so that the concept is correctly implemented and is robust and reliable for any/all Properties in any structural form (top level or multiply nested).

Any advice/assistance will be greatly appreciated!

Jeronymite
  • 51
  • 1
  • 2
  • 11
  • Is PropertyGrid loading class properties by itself? no Binding included? How do you recognize if any propery was changed? – o_w Aug 05 '20 at 12:51
  • What is the "real" object and what is the clone in your rather convoluted code sample? Please provide a [*minimal*](https://stackoverflow.com/help/minimal-reproducible-example) example of your issue. – mm8 Aug 05 '20 at 13:31
  • The "real" object is _pclPropertiesSource_ and the clone is _pclPropertiesClone_. I pass the real object to the grid instantiation method. It then clones it and uses the clone for all UI changes to the properties, and only updates the real object when OK. I will edit the example to reduce "noise" associated with buttons/functions not really relevant for the purposes of this post. The _OnPropertyChanged_ event handler handles changes. Thanks for looking at this issue. – Jeronymite Aug 05 '20 at 20:32
  • Code edited to remove various irrelevant sections. – Jeronymite Aug 05 '20 at 20:40
  • *Don't use Hungarian Notation!* Nobody uses that anymore, and I do mean *nobody.* – Robert Harvey Aug 05 '20 at 20:40
  • Thanks, Robert. Noted. – Jeronymite Aug 05 '20 at 20:46

1 Answers1

0

Further research shows that the problem stems from the Clone operation. The code uses MemberwiseClone() which is a shallow clone. To be able to see nested properties, one needs to do a deep clone. Using a deep clone has enabled the code to work as expected. More information on deep cloning can be found here: How to deep copy an object

Jeronymite
  • 51
  • 1
  • 2
  • 11