Giving Property Display Names Icons

Grids for WPF Forum

Posted 7 years ago by Mark W. Stroberg
Version: 14.1.0602
Avatar

Hello. I am using the 2014 Quarter 1 Actipro PropertyGrid control. I wondered if there was any way, in the first column elements (DisplayName), there was a way of adding an icon as well as the Display Name? I imaging it requires overriding a data template, or something like that, but I have no idea how to do this for the PropertyGrid.

Thanks in advance.

   Mark W. Stroberg

Comments (23)

Posted 7 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Hi Mark,

Each property has DataTemplates that can be set for the name or value columns.  Normally people just modify the value column template so that they can introduce custom edit controls for various property types.  But each property also has a NameTemplate property that can be set to a custom DataTemplate, which could contain an Image and TextBlock together.

If you make a custom data factory (class that inherits TypeDescriptorFactory and will be set to PropertyGrid.DataFactory), override its CreatePropertyDataAccessor method, and have that effectively be:

protected override IPropertyDataAccessor CreatePropertyDataAccessor(IPropertyDataAccessor parent, object value, PropertyDescriptor propertyDescriptor) {
	return new CustomPropertyDescriptorDataAccessor(parent, value, propertyDescriptor);
}

Then make a custom CustomPropertyDescriptorDataAccessor class that inherits PropertyDescriptorDataAccessor.  In that, you can override the NameTemplateKeyInternal property to return some resource key, which will be the x:Key of whatever DataTemplate for the name column that you create and put in Application.Resources.  Those steps will get you a custom name column DataTemplate.

Then you need to somehow indicate the URL for each property's icon.  I would set that on a property on CustomPropertyDescriptorDataAccessor.  Then have your DataTemplate's Image bind to it with a binding like:

{Binding ImageUrl, RelativeSource={RelativeSource AncestorType={x:Type propgridPrimitives:IPropertyDataAccessor}}}

Hope that helps!


Actipro Software Support

Posted 7 years ago by Mark W. Stroberg
Avatar

Thanks for the quick response. I have pieced together some code that builds without errors. Unfortunately, it does not function properly. Here is my resource declaration for the actual Name column data template:

        <DataTemplate x:Key="IconPropGridNameColumnDataTemplate">
            <StackPanel Orientation="Horizontal">
                <Image Width="16" Height="16" Margin="0.0,0.0,5.0,0.0" Source="{Binding ImageUrl, RelativeSource={RelativeSource AncestorType={x:Type propgrid:IPropertyDataAccessor}}}" />
                <TextBlock Padding="0.0,0.0,0.0,0.0" Text="{Binding DisplayName, RelativeSource={RelativeSource AncestorType={x:Type propgrid:IPropertyDataAccessor}}}" />
            </StackPanel>
        </DataTemplate>

Here is my declaration assigning the custom data factory:

                    <propgrid:PropertyGrid.DataFactory>
                        <local:IconPropGridTypeDescriptorFactory />
                    </propgrid:PropertyGrid.DataFactory>

Here are my custom data factory and custom property data accessor class definitions:

    public class IconPropGridTypeDescriptorFactory : TypeDescriptorFactory
    {
        public IconPropGridTypeDescriptorFactory()
            : base()
        {
        }

        protected override IPropertyDataAccessor CreatePropertyDataAccessor(IPropertyDataAccessor parent, object value, PropertyDescriptor propertyDescriptor)
        {
            return new IconPropertyDescriptorDataAccessor(parent, value, propertyDescriptor);
        }
    }

    public class IconPropertyDescriptorDataAccessor : PropertyDescriptorDataAccessor
    {
        public IconPropertyDescriptorDataAccessor(IPropertyDataAccessor parent, object value, PropertyDescriptor propertyDescriptor)
            : base(parent, value, propertyDescriptor)
        {
        }

        protected override object NameTemplateKeyInternal { get { return "IconPropGridNameColumnDataTemplate"; } }
        public object ImageUrl { get { return new Uri("pack://application:,,,/WiXBench.UI.UserControls;component/Images/Error.gif", UriKind.RelativeOrAbsolute); } }
    }

 

A few observations:

I realize that the data factory class does not need a default constructor, but I put it in after failure for debugging purposes, just to see if the custom data factory was getting instantiated. Answer: It is getting instantiated and the constructor is called. However, even if there are errors in my data accessor class, I have no way of even determining that because the data factory's override of the CreatePropertyDataAccessor() function NEVER GETS CALLED. Is there some property I have to set to enable use of a custom data accessor class?

Although this has no bearing on the problem at hand, could you look at my Data Template and see if I am doing the binding for the TextBlock and Image components correctly?

The sample Image URI that I put into the custom PropertyDescriptorDataAccessor class is just for testing purposes. My goal is to make the images dependent on the actual property grid row.

Perhaps you could look at this code and see if there is something I am obviously doing wrong. You have already been extremely helpful. I believe I am close, I just need a little more guidance.

Thanks in advance.

Mark W. Stroberg

[Modified 7 years ago]

Posted 7 years ago by Mark W. Stroberg
Avatar

Just a quick note: I found a way to do it with individual data templates for each property item, by registering a DependencyProperty. It works great, but suffers from the fact that I have to use a separate data template for each property, so that I can specify the file name for the icon in the data template image component. I would much rather use the method you have given me, as it is much less cumbersome, and only requires the single data template declaration. If you can show me how to make it work, I would greatly appreciate it.

Thanks.

Mark W. Stroberg

Posted 7 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Hi Mark,

There are several kinds of CreateXXX methods on the data factory and each one is called based on what kind of property is being loaded.  The one I pointed you to is for normal properties.  There are other methods for collection properties and merged (when you select more than one object in PropertyGrid) properties.  But considering you said you eventually got the DataTemplate working, I assume you sorted this out?

For the DataTemplate, it looks fine although you might want to vertically center the TextBlock.

Ok back on making the DataTemplate more dynamic, I think I see the problem.  The current PropertyGrid control design has several "layers" in it, and the PropertyDescriptorDataAccessor you created isn't directly getting used in the DataTemplate's data context.  There is another PropertyGridDataAccessorItem that is the "container" for that PropertyDescriptorDataAccessor, and I believe that is what IPropertyDataAccessor you are hitting with the bindings.  PropertyGridDataAccessorItem implements IPropertyDataAccessor and "wraps" PropertyDescriptorDataAccessor and exposes a lot of its properties for data binding.  But since it's a wrapper, your ImageUrl property won't show up on it.  I didn't try this I don't have a sample here of this scenario but I wonder if you tried this (navigating to the DataContext), if it would work:

{Binding DataContext.ImageUrl, RelativeSource={RelativeSource AncestorType={x:Type propgridPrimitives:IPropertyDataAccessor}}}

I would also like to mention that for our upcoming 2017.1 version, we rewrote PropertyGrid mostly from scratch (same features and concepts though) and made it much faster, leaner, and easier to customize.  There is no more wrapping of "layers", and the property data accessor you created (now called property models in the new version) are the actual DataContext for your DataTemplates.  No more RelativeSource in the Binding is needed.  Meaning if you did this scenario in the 2017.1 version, I believe your binding would just be: "{Binding ImageUrl}".  I think you'll like the new version once it's out.


Actipro Software Support

Posted 7 years ago by Mark W. Stroberg
Avatar

No, I did not get the PropertyGrid wide DataTemplate working, I used a completely different method. As I stated, we are not even getting to the point of testing the DataAccessor class, as the IconPropGridTypeDescriptorFactory.CreatePropertyDataAccessor function is never being called, so I never use the custom IconPropertyDescriptorDataAccessor class. You implied that it was not being called because I did not have "Normal" properties. What does that mean, exactly? I don't believe my properties are either Collection or Merged. I am almost certain that I do not allow more than one to be selected and each property is bound to a single object. How would I get this to work? If you can tell me how to alter my properties so that this function will be called, I would appreciate it. Below I have an example of one of my Property Item declarations in XAML:

                    <propgrid:PropertyGridPropertyItem Category="Information" DisplayName="MSI Type Number" Value="{Binding MSITypeNumber}"  Description="{x:Static r:CustomActionViewControl.Custom_Action_MSITypeNumber_Attribute_Description}" IsReadOnly="True" />

 This is a simple string property. Is it not normal? Is there some flag I have to set to make it non-Merged (although I do not know if I can select more than  one property in the grid, I have nevert tried to)?

Thanks.

   Mark W. Stroberg

Posted 7 years ago by Mark W. Stroberg
Avatar

One quick note: The license we have for the PropertyGrid is 2014 Q1. While we are considering purchasing a license for the whole bundle, newest version, at some point, that is not in the cards right now.

Answer - Posted 7 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Hi Mark,

Oh I see, you are declaring the properties right directly in XAML.  I assumed you were binding to some view-model object and letting us generate the properties for you via the data factory.  That explains why the data factory isn't called.  Without a simple sample to look at, my guess is you would want to do the same sort of thing where you make a custom class that inherits PropertyGridPropertyItem and add your ImageUrl property.  Then use that class instead in your XAML and in the DataTemplate, use the Binding in my previous reply.  You don't need a data factory in this scenario.


Actipro Software Support

Posted 7 years ago by Mark W. Stroberg
Avatar

I understand that I can change the class reference in the actual PropertyGridPropertyItem declarations in XAML, but what is the exact XAML syntax for setting the Property Item in the ProperyGrid to use my class? I tried the folowing (my class is named "PropertyGridIconPropertyItem"):

                    <propgrid:PropertyGrid.ItemTemplate>
                        <local:PropertyGridIconPropertyItem />
                    </propgrid:PropertyGrid.ItemTemplate>

That did not work, as it gave me a parse exception.. Forgive me, as I am still a newbie at WPF and XAML.I really need some help with this.

Also, my alternate method of settiing Name column templates that woked for me the other day required that I declare a DataTemplate for the Name column in each property item declaration. Now that I have a separate class for my property items, how can I make use of this to require only one Name column DataTemplate declaration to be used by all instances of my class (perhaps somewhere in my derived class [but there is no XAML file for it])?

Thanks.

   Mark W. Stroberg

Posted 7 years ago by Mark W. Stroberg
Avatar

Stupid me. I forgot to enclose the declaration in a DataTemplate wrapper. I changed the above code to:

                    <propgrid:PropertyGrid.ItemTemplate>
                        <DataTemplate>
                            <local:PropertyGridIconPropertyItem />
                        </DataTemplate>
                    </propgrid:PropertyGrid.ItemTemplate>

and it seems to function properly.

I would still appreciate an answer to my last question.

Thanks.

Mark W. Stroberg

[Modified 7 years ago]

Posted 7 years ago by Mark W. Stroberg
Avatar

I have examined methods of creating a DataTemplate in code and applying it to a control programmatically. I have a question about doing this however: Where and when is the best place to apply it? I put a breakpoint in the constructor of my derived PropertyGridPropertyItem class (affter calling base class constructor), and noted that at that point the NameTemplate property was still null. It seems to me, that that means if I were to apply the DataTemplate there, it would simply be overwritten by the default template object at a later time. Is there an event or virtual function which would be the best place to apply it, or am I wrong and it would work in the constructor? My preference would be simply to do this in XAML somehow, but I haven't a clue as to how to apply a XAML DataTemplate to all instances of my derived class as the default. It woud be nice if this could be done.

Any help would be appreciated.

Thanks.

   Mark W. Stroberg

Posted 7 years ago by Mark W. Stroberg
Avatar

I have figured out a good way of applying the NameTemplate DataTemplate to all instances of my derived PropertyGridIconPropertyItem class. I do it in code, instead of with XAML. I would still like to know how to declare and apply this template using XAML, for curiosity's sake, but am quite proud that I was able to figure it out using C# code. First, as I stated before, I set the Property Item template of the Grid to my class:

                    <propgrid:PropertyGrid.ItemTemplate>
                        <DataTemplate>
                            <local:PropertyGridIconPropertyItem />
                        </DataTemplate>
                    </propgrid:PropertyGrid.ItemTemplate>

Then I add the setting of a property in my class called ImageFileName to a string literal, as in the folowing XAML declaration of one of my simpler property items:

                    <local:PropertyGridIconPropertyItem Category="Information" DisplayName="MSI Type Number" Value="{Binding MSITypeNumber}"  Description="{x:Static r:CustomActionViewControl.Custom_Action_MSITypeNumber_Attribute_Description}" IsReadOnly="True" ImageFileName="CAView_Prop_MSITypeNumber.gif"/>

That is all I have to do in XAML. Here is my PropertyGridIconPropertyItem class, derived from PropertyGridPropertyItem:

    public class PropertyGridIconPropertyItem : ActiproSoftware.Windows.Controls.PropertyGrid.PropertyGridPropertyItem
    {
        private static DataTemplate nameTemplateInstance;

        public PropertyGridIconPropertyItem () : base ()
        {
            this.Loaded += PropertyGridIconPropertyItem_Loaded;
        }

        void PropertyGridIconPropertyItem_Loaded(object sender, RoutedEventArgs e)
        {
            NameTemplate = NameTemplateInstance;
        }

        public object ImageURI { get { return new Uri("pack://application:,,,/WiXBench.UI.UserControls;component/Images/" + ImageFileName, UriKind.RelativeOrAbsolute); } }
        public string ImageFileName { get; set; }

        private DataTemplate NameTemplateInstance
        {
            get
            {
                if (nameTemplateInstance == null)
                {
                    System.Windows.Data.Binding binding;

                    nameTemplateInstance = new DataTemplate();
                    nameTemplateInstance.DataType = typeof(PropertyGridIconPropertyItem);

                    FrameworkElementFactory spFactory = new FrameworkElementFactory(typeof(StackPanel));
                    spFactory.SetValue(StackPanel.OrientationProperty, System.Windows.Controls.Orientation.Horizontal);

                    FrameworkElementFactory imFactory = new FrameworkElementFactory(typeof(Image));
                    imFactory.SetValue(Image.WidthProperty, 16.0);
                    imFactory.SetValue(Image.HeightProperty, 16.0);
                    imFactory.SetValue(Image.MarginProperty, new Thickness(0.0, 0.0, 5.0, 0.0));
                    binding = new System.Windows.Data.Binding();
                    binding.Path = new PropertyPath("DataContext.ImageURI");
                    binding.RelativeSource = new RelativeSource(System.Windows.Data.RelativeSourceMode.FindAncestor, typeof(IPropertyDataAccessor), 1);
                    imFactory.SetValue(Image.SourceProperty, binding);
                    spFactory.AppendChild(imFactory);

                    FrameworkElementFactory tbFactory = new FrameworkElementFactory(typeof(TextBlock));
                    tbFactory.SetValue(TextBlock.PaddingProperty, new Thickness(0.0, 0.0, 0.0, 0.0));
                    binding = new System.Windows.Data.Binding();
                    binding.Path = new PropertyPath("DisplayName");
                    binding.RelativeSource = new RelativeSource(System.Windows.Data.RelativeSourceMode.FindAncestor, typeof(IPropertyDataAccessor), 1);
                    tbFactory.SetValue(TextBlock.TextProperty, binding);
                    spFactory.AppendChild(tbFactory);

                    nameTemplateInstance.VisualTree = spFactory;
                }
                return nameTemplateInstance;
            }
        }
    }

Note that I am using a singleton instance of the DataTemplate, so as to avoid creating numerous DataTemplate objects, when only one is necessary. In the constructor, I set up a handler for the Loaded event, as load time seems to be the best time to assign the DataTemplate I have created to the NameTemplate property. I assign the NameTemplate property in the Loaded event handler.

When I first wrote the code to create the DataTemplate object, it did not work correctly. By carefully testing with hard coded values for the Image SourceURI and TextBlock Text, I determined that the layout was correct. I could not, however, get the property binding to work properly. After some investigation into the System.Windows.Data.Binding class, I was able to figure out that the Binding required setting both the Path and RelativeSource properties, and I figured out how to do that for both the TextBlock Text and Image Source. It works flawlessly, and, unless there is a very simple XAML technique to do this, I think I will stick with what I have.

You mentioned vertically centering the TextBlock Text, but I have tried it in several System Font DPI settings, and the text is already centered perfectly. I noticed, however, that the Icon Image is actually not vertically centered properly. I tried setting the VerticalAlignment property of the Image to Center, and it made no difference. I even set it to Bottom, and it did not affect the vertical position. It is slightly biased toward being in the top of the cell.

Thank you for all your help. Again, if you can demonstrate to me a good, simple XAML solution, I would really like to know how to do this, and wish to learn more. I am going to mark your first comment that offered the derived PropertyGridPropertItem class as a solution.

   Mark W. Stroberg

[Modified 7 years ago]

Posted 7 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Hi Mark,

I'm glad you have it going but I'm not sure what you have is optimal.  For scenarios like this, it would be easier for us to understand if you can put together a new simple sample that shows what you're trying to do and email that to our support address.  Then we can debug with that, tweak it as needed and send you back what the changes are.  That would eliminate any questions on both sides and would give us the full context of what you're doing.  The sample should be pretty minimal and just show the problem only.

I think with a small sample, we could show you a better way of accomplishing what you want.  Please email us with one and in the email, reference this thread and be sure to rename the .zip file extension of what you send so it doesn't get spam blocked.  Thanks!


Actipro Software Support

Posted 7 years ago by Mark W. Stroberg
Avatar

I realize that creating and applying a DataTemplate in code is not the most elegant way to do it, which is why I asked for a XAML solution. However, any XAML solution must be applicable to ALL instances of the derived PropertyGridPropertyItem class, or it is not useful to me.

I have created a basic sample Visual Studio 2013 project, which illustrates the solution I came up with. What I am trying to do is display an icon in the Name column to the left of the DisplayName text. What I have works perfectly, but if you can come up with a XAML solution to applying the DataTemplate to the NameTemplate property, I am open to suggestions.

Please instruct me as to how to get this project to you. I do not see any "Attach File" buttons or links in the forum.

Thanks.

   Mark W. Stroberg

Posted 7 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Hi Mark,

Please email it to our support address.  In the email, reference this thread and be sure to rename the .zip file extension of what you send so it doesn't get spam blocked.  Thanks!


Actipro Software Support

Posted 7 years ago by Mark W. Stroberg
Avatar

I used the support ticket form on your website. I have attached zipped solution to it. I know you may not get back to me until tommorow, but you have been quite helpful.

Ticket # [11A-2038F73F-490E]..

Thanks again.

   Mark W. Stroberg

[Modified 7 years ago]

Posted 7 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Hi Mark,

Thanks for the sample.  If you do this in XAML (and remove all the PropertyGridIconPropertyName codebehind except for the two Image* properties), it seems to work great:

<propgrid:PropertyGrid x:FieldModifier="public" 
			VerticalAlignment="Stretch" HorizontalAlignment="Stretch" 
			x:Name="propertiesGrid" SummaryCanAutoSize="True"
			IsEnabled="True">
	<propgrid:PropertyGrid.Resources>
		<DataTemplate x:Key="ImageNameTemplate">
			<StackPanel Orientation="Horizontal">
				<Image Margin="0,0,5,0" Width="16" Height="16" Source="{Binding DataContext.ImageURI, RelativeSource={RelativeSource AncestorType={x:Type propgrid:IPropertyDataAccessor}}}" />
				<TextBlock Text="{Binding DisplayName, RelativeSource={RelativeSource AncestorType={x:Type propgrid:IPropertyDataAccessor}}}" VerticalAlignment="Center" />
			</StackPanel>
		</DataTemplate>
	</propgrid:PropertyGrid.Resources>

    <!-- Test Category -->
    <local:PropertyGridIconPropertyItem Category="Test Category" DisplayName="Script" Value="{Binding Script}"  NameTemplate="{StaticResource ImageNameTemplate}"
										Description="This property is a dummy property used to test the PropertyGridIconPropertyItem class." ImageFileName="Prop_Script.gif"/>

</propgrid:PropertyGrid> 

Then you can have multiple instances of your property items reference the same NameTemplate too. 


Actipro Software Support

Posted 7 years ago by Mark W. Stroberg
Avatar

I thought you could do something like that, but I wasn't quite sure how to do it. It is not, however, completely satisfying, because it still requires referencing the NameTemplate in each of the the PropertyItem XAML declarations. Isn't there a simple way of declaring the DataTemplate once for my derived class, such that it is the default NameTempate used by my class, so that I do not have to reference it at all in the XAML declarations? When I do it in code, it is encapsulated into the class, and does not require multiple XAML resource declarations for multiple PropertyGrid instances, or any reference to the NameTemplate in the individual PropertyItem XAML declarations. Is it possible to do this, or is what I am asking simply not something that can be done?

By the way, I see that I do not need to set the item template in XAML in the Property Grid to use my derived class. Simply putting the derived class name in the Property Item declarations is sufficient. That is good. I just need that last step of setting one XAML declaration of the Name Template to be used as the default for all instances of my derived class, if that is possible. How does the base PropertyGridPropertyItem class set it's default name template?

Thanks.

   Mark W. Stroberg

[Modified 7 years ago]

Posted 7 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Hi Mark,

If you take NameTemplate="{StaticResource ImageNameTemplate}" out, you could have this in code-behind.

public class PropertyGridIconPropertyItem : PropertyGridPropertyItem {
	private static DataTemplateSelector nameTemplateSelector = new CustomNameTemplateSelector();

	public PropertyGridIconPropertyItem()
		: base() {
		this.NameTemplateSelector = nameTemplateSelector;
	}

	public object ImageURI { get { return new Uri("pack://application:,,,/Images/" + ImageFileName, UriKind.RelativeOrAbsolute); } }
	public string ImageFileName { get; set; }

}


public class CustomNameTemplateSelector : DataTemplateSelector {

	public override DataTemplate SelectTemplate(object item, DependencyObject container) {
		var element = container as FrameworkElement;
		if (element != null)
			return element.TryFindResource("ImageNameTemplate") as DataTemplate;

		return null;
	}

}


Actipro Software Support

Posted 7 years ago by Mark W. Stroberg
Avatar

This works, and it is almost, but not quite, what I want. It would be nice if I could put the DataTemplate resource in a central location, declared only once, and not in each PropertyGrid XAML instance. If this can be done, I would have the complete solution. Also, I could use this knowledge and apply it to other issues.

Thanks.

   Mark W. Stroberg

Posted 7 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

You can put it in Applicaiton.Resources instead of PropertyGrid.Resources.  That would work fine for all PropertyGrid instances.


Actipro Software Support

Posted 7 years ago by Mark W. Stroberg
Avatar

This works perfectly in my test program, as it is an appliication, and there is an App.xaml file to put application resources in. However, the actual code this is designed for is in a class library (DLL), which has no App.xaml in its project. I placed it in the resources of the application which loads the class library, and that works, but I would really like to place it in the class library somewhere, so that any application using it would not require a reference to the Property Grid component. Do you know where the appropriate place to put it is, if it is to be declared in a class library?

[Modified 7 years ago]

Answer - Posted 7 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Resource lookup in WPF basically walks up the visual tree until it hits a Window.  After that, it looks at the Application.Resources.  You could make a line in your class library that injects a custom ResourceDictionary (defined in your class lib and containing the DataTemplate) into your Application.Resources's MergedDictionaries.  That's a little complex but would work globally.  Or alternatively you can add the DataTemplate to the Window.Resources of each window that uses a PropertyGrid.


Actipro Software Support

Posted 7 years ago by Mark W. Stroberg
Avatar

There is a highest level UserControl object which contains all the PropertyGrid instances as descendants in my class library. I put it there, and everything works perfectly.

II can't thank you guys enough. You went out of your way to help me, even though I may have asked some pretty dumb questions. What I have learned from you about WPF will help me greatly in the future.

   Mark W. Stroberg

The latest build of this product (v24.1.2) was released 0 days ago, which was after the last post in this thread.

Add Comment

Please log in to a validated account to post comments.