How may I set the display template for a read-only PropertyDefinition?

1 Answer 419 Views
PropertyGrid
Joe
Top achievements
Rank 2
Iron
Iron
Veteran
Joe asked on 29 Jul 2021, 10:22 PM
In my PropertyGrid, one of my PropertyDefinitions binds to a property that returns a custom struct.  The struct, named "Length", represents a measured length with numeric value and measurement unit tied together. 

Since this is read-only I don't want an editor template, but I do want to be able set the template that's used to show the read-only  value.  

I do have a DataTemplate all ready.  It just shows a Label and uses a MultiBinding with the Length to show some custom information. But I cannot make the PropertyDefinition use it.

I tried setting the EditorTemplate property to my DataTemplate.  It seemed to have no effect.   Unfortunately there does not appear to be any sort of ReadOnlyTemplate property.  So is there a way for me to customize the ReadOnly view?

1 Answer, 1 is accepted

Sort by
0
Accepted
Dilyan Traykov
Telerik team
answered on 02 Aug 2021, 02:51 PM

Hello Joe,

By your description, I assume you're using the Single EditMode of the control. Please let me know if I'm correct in this assumption. If that is not the case, setting the EditorTemplateSelector property when using auto-generated property definitions, or using the EditTemplate of the defined PropertyDefinition works as expected at my end.

If you are in fact using the Single edit mode, we already have an open feature request regarding a similar requirement which you can find and vote for here: PropertyGrid: Expose a ReadOnlyTemplate mechanism. Currently, the PropertyGridField generates a TextBlock whose text is bound to the underlying property (the Lenght in your case).

One approach I can suggest having this in mind would be to override the ToString method of the struct to return the desired value.

        public override string ToString()
        {
            return this.Value + " " + this.Unit;
        }

Please let me know if this would work for you.

For your convenience, I've prepared a small sample project to demonstrate this in action. I hope you find it helpful.

Regards,
Dilyan Traykov
Progress Telerik

Virtual Classroom, the free self-paced technical training that gets you up to speed with Telerik and Kendo UI products quickly just got a fresh new look + new and improved content including a brand new Blazor course! Check it out at https://learn.telerik.com/.

Joe
Top achievements
Rank 2
Iron
Iron
Veteran
commented on 02 Aug 2021, 05:31 PM | edited

Hi Dilyan,

No I am not using it in Single Edit Mode.  But my binding still isn't working. 

The source of the problem appears to be that my EditorTemplate uses a MultiBinding.  I tried changing your example to also use a MultiBinding and had the exact same problem:  When the EditorTemplate uses a MultiBinding,   the "getter" of the property in question is never called at all.  So the MultiConverter.Convert function ends up getting a NULL value for the thing I'm trying to bind to. 

But when I remove the EditorTemplate, it works again.

I can update your example to show what I mean (It just took me a minute but let me illustrate with myu own code.  Here is the structure that's being used for the property (at least a part of it).  It also has a ToString() method but that's not going to be good enough for my purposes

public struct Length
{
    public        double     Value   { get; set; }
    public        LengthUnit Unit    { get; set; }  // An enum indicating unit
}
Here is the PropertyGrid definition.  The two properties it shows are instance of this Length structure. As you can see I have one the bindings using my EditorTemplate and the other not using it.

 

<DataTemplate DataType="{x:Type items:LevelItem}">
    <StackPanel>
        <tk:RadPropertyGrid Item="{Binding}" Style="{StaticResource BasePropertyGridStyle}" >
            <tk:RadPropertyGrid.PropertyDefinitions>
                <tk:PropertyDefinition Binding="{Binding LeveledSurface.Width}"   IsReadOnly="True" DisplayName="{x:Static common:CommonStrings.Width}"/>
                <tk:PropertyDefinition Binding="{Binding LeveledSurface.Height}"  IsReadOnly="True" DisplayName="{x:Static common:CommonStrings.Height}"  EditorTemplate="{StaticResource HeightTemplate}"/>
            </tk:RadPropertyGrid.PropertyDefinitions>
        </tk:RadPropertyGrid>
    </StackPanel>

If you look at the first attached image ("snap8.png") you will see that the property that uses the template shows nothing but the property that does not use it shows the result of Length.ToString().

Here is the EditorTemplate I'm trying to use.  It takes the Length Value as well as two other values in a MultiBinding to fill out a TextBlock.Text value

<DataTemplate x:Key="HeightTemplate" DataType="{x:Type net:Length}">
    <TextBlock>
        <TextBlock.Text>
            <MultiBinding Converter="{common:LengthToStringConverter DebugMode=True}">
                <Binding />
                <Binding Source="{svc:System}" Path="LengthUnitZ.Unit"/>
                <Binding Source="{svc:System}" Path="LengthUnitZ.Precision"/>
            </MultiBinding>
        </TextBlock.Text>
    </TextBlock>
</DataTemplate>

I tried putting a breakpoint into the converter that the MultiBinding is using, the passed in array of 3 values shows the first one (the 'Length' value) to be NULL.  The remaining two values are fine.    Since the property getter is never called (I put a breakpoint there to verify), this makes sense.  I just don't understand why the getter isn't being called for my template.

If I remove the EditorTemplate, then my property getter is called and the "Height" bound value shows up just like the "Width".  I've attached two images showing these values in my UI one with the editor Template and one without.

So it would seem that my first line of the MultiBinding -- which just is literally this:

<Binding/>

Is failing to get at the value.  I do this all the time in DataTemplates so I don't understand why this does not work here.

I have tried specifying "OneWay" on this binding as well as on the binding in the PropertyDefinition.  I've tried specifying  IsReadOnly=True in the Property Definition and not specifying it.  No combination of any of these steps has any effect

Why is my Binding not called in this case?

Dilyan Traykov
Telerik team
commented on 03 Aug 2021, 11:31 AM

Thank you for the provided clarifications, code snippets, and images.
I tried setting up the MultiBinding as you explained in the sample project, however, the template is still displayed as expected at my end. I'm attaching the updated project to my reply for your reference. Can you please have a look and let me know if I'm missing something of importance?
If that is the case, please let me know what modifications I need to make in order to replicate the issue and I will gladly assist you further.
I will be awaiting your reply.

Joe
Top achievements
Rank 2
Iron
Iron
Veteran
commented on 04 Aug 2021, 05:51 PM

Hi Dilyan,

I modified your first sample you posted to be more like my code to show you what I mean.  Here are the changes I made:

  1. I changed the Length class to add an integer precision value.  I also changed its ToString() method to return "THIS SHOULD NOT BE CALLED"
  2. I changed the View to show the items list in a ListBox.  The ItemTemplate for the ListBox is now what shows the property grid which uses "LengthTemplate" as the EditorTemplate
  3. I changed the LengthConverter to be an IMultiValueConverter that takes the unit, a length value and a precision.
  4. I changed the binding for the LengthTemplate (which expects to get a Length object) to pass all 3 of the Length object's properties on to the MultiValueConverter

 

In this case, the EditorTemplate is never used. Instead the ToString() method is called.  

Dilyan Traykov
Telerik team
commented on 05 Aug 2021, 01:19 PM

Thank you for the updated project.
I noticed that the updated RadPropertyGrid definition had the EditMode property set to Single. As mentioned in my original reply, in this case, the only way to achieve the desired formatting is to override the ToString method of the struct.
If this was, however, left involuntary (as you did mention that you're not using the Single EditMode in one of your latter replies) you can achieve the expected result with a few modifications:

  1. Change the second cast in the converter from double to int (as this is the Value property type in the Length struct). Alternatively, you can use the TryParse method.
  2. Add Length. to the binding paths for them to be evaluated correctly - <Binding Path="Length.Unit" />
  3. Remove the EditMode setting.

For your convenience, I'm also attaching the modified project. Please let me know if this provides the desired result.

Joe
Top achievements
Rank 2
Iron
Iron
Veteran
commented on 12 Aug 2021, 07:37 PM | edited

Hi Dilyan,

I am sorry for wasting your time by missing the EditMode thing.   Also sorry for the delay in responding; I got pulled on to other things at work.

This time I was determined to make the example work a lot like my actual application.  My app layout is quite a bit more complicated.  So I've changed it to be this

The List Box ItemTemplate doesn't just show a RadPropertyGrid anymore.  Instead it shows a RadDropDownButton.  The DropDownButton contains a ContentControl which sets its Content to the current Binding.  That Binding, in turn should be the item from the ListBox (i.e. an instance of class Item). 

I realize that's completely unnecessary here but my actual layout is complicated and needs something like that.  Regardless it  it still should be legal

I define a Default DataTemplate for type Item higher up the tree.  The idea is that the ContentControl to pick it up.  And it does.  I can see that because the PropertyGrid does manage to show the Name value properly.  But the Length value is now blank.

I hope I managed to illustrate the problem here.

Dilyan Traykov
Telerik team
commented on 16 Aug 2021, 02:21 PM

Hi Joe,
Thank you very much for the updated project.
Upon investigation, I noticed that the LengthConverter returns DependencyProperty.UnsetValue in all scenarios.
I was then able to display the desired value by making the following modification inside the converter:

            if (!(values[0] is string unit) ||
                !(values[1] is double value) ||
                !(values[2] is int precision))

Can you please also try this at your end and let me know if it displays the expected value?

Joe
Top achievements
Rank 2
Iron
Iron
Veteran
commented on 17 Aug 2021, 08:53 PM

HI Dilyan,

I apologize for my multiple errors.  You are right, the fixes made it work.   But this still did not explain what was happening with my code.   

But then I remembered that in my code, I'm passing the Length structure as a whole to the MultiBinding.  I'm not extracting items from it as this example did.  So I changed the example to do that and it broke again.  More to the point its now failing similarly to how it fails in my application. I can see the MultiBinding receiving the completely wrong data type

Here's the MultiBinding (and converter).  They now take 2 arguments: a Length and a precision (int)

<sys:Int32 x:Key="Precision">3</sys:Int32>

<DataTemplate x:Key="HeightTemplate" DataType="{x:Type local:Length}">
    <Grid Margin="2">
        <TextBlock Foreground="Red">
            <TextBlock.Text>
                <MultiBinding Converter="{StaticResource LengthConverter}">
                    <Binding />
                    <Binding Source="{StaticResource Precision}"/>
                </MultiBinding>
            </TextBlock.Text>
        </TextBlock>
    </Grid>
</DataTemplate>


I put Logging in the converter to dump out what types it was given in the event of an error

if (!(values[0] is Length length) ||
    !(values[1] is int precision))
{
    Debug.WriteLine($"ERROR: Types were \'{values[0]?.GetType().FullName}\', and \'{values[1]?.GetType().FullName}\'");
    return DependencyProperty.UnsetValue;
}

 

And here is what it dumped out:

ERROR: Types were 'ReadOnlyTemplate.Item', and 'System.Int32'
ERROR: Types were 'ReadOnlyTemplate.Item', and 'System.Int32'
So the MultiBinding is getting ReadOnlyTemplate.Item instead of Length.  In my application, I do not get "ReadOnlyTemplate.Item".  But I get something equally wrong (the parent view-model)

For some reason, using <Binding/> as the first argument to the MultiBinding fails to pick up the DataContext of the template (type Length). as I would expect it to.    

 

I have attached the latest solution with these changes.  Thank you for being patient with me

 

Joe
Top achievements
Rank 2
Iron
Iron
Veteran
commented on 18 Aug 2021, 02:42 PM | edited

OK, I think I have finally figured out a workaround.  But I still don't understand why my original approach did not work.   The rule is this:

When I use the PropertyDefinition.EditorTemplate property, I cannot simultaneously use the PropertyDefinition.Binding property

I thought I could make the Property Definition bind to type Length and then have the DataTemplate accept that as its DataType.  But instead, I must remove the PropertyDefinition.Binding setting completely and just use a DataTemplate that binds to the parent object, (of type local:Item in this example)

To be specific I changed the PropertyDefinition (editor template inline) from this.  I have bolded the key differences

<telerik:PropertyDefinition IsReadOnly="True" Binding="{Binding Length}" DisplayName="Height" >
    <telerik:PropertyDefinition.EditorTemplate>
        <DataTemplate DataType="{x:Type local:Length}">
            <Grid Margin="2">
                <TextBlock Foreground="Red">
                    <TextBlock.Text>
                        <MultiBinding Converter="{StaticResource LengthConverter}">
                            <Binding />
                            <Binding Source="{StaticResource Precision}"/>
                        </MultiBinding>
                    </TextBlock.Text>
                </TextBlock>
            </Grid>
        </DataTemplate>
    </telerik:PropertyDefinition.EditorTemplate>
</telerik:PropertyDefinition>

to this:
<telerik:PropertyDefinition IsReadOnly="True"  DisplayName="Height">
    <telerik:PropertyDefinition.EditorTemplate>
        <DataTemplate DataType="{x:Type local:Item}">
            <Grid Margin="2">
                <TextBlock Foreground="Red">
                    <TextBlock.Text>
                        <MultiBinding Converter="{StaticResource LengthConverter}">
                            <Binding Path="Length" />
                            <Binding Source="{StaticResource Precision}"/>
                        </MultiBinding>
                    </TextBlock.Text>
                </TextBlock>
            </Grid>
        </DataTemplate>
    </telerik:PropertyDefinition.EditorTemplate>
</telerik:PropertyDefinition>

Is that is what's supposed to be required?
Dilyan Traykov
Telerik team
commented on 19 Aug 2021, 09:13 AM

Thank you for the provided code snippets.
The DataContext of each property field is the Item to which the RadPropertyGrid is bound to.
With this said, when the Path of the Binding in the LengthConverter is not set, the entire Item object will be passed. As you've correctly concluded, you need to set the Path to Length for the converter to work as expected.
At my end, it is not necessary to change the DataType of the template (actually, it can be omitted) or remove the Binding of the PropertyDefinition.
Nonetheless, I do hope you've now achieved the desired result. Do let me know if any further questions or concerns arise.

Tags
PropertyGrid
Asked by
Joe
Top achievements
Rank 2
Iron
Iron
Veteran
Answers by
Dilyan Traykov
Telerik team
Share this question
or