Resettable attribute (was IsNullable property types)

Jan 10, 2011 at 8:54 PM

I think this should be my last issue: Because of the nature of my application design, all of my properties must be nullable. One side effect of this is that when the PropertyEditor code sees a property type that is of type System.Nullable<type> it atuomagically puts a checkbox on the dialog form in front of the control to edit the property. This isn't really applicable to my design, so can this be turned off somehow?

What happens when the checkbox is clicked, however, is applicable. If the checkbox, the property is cleared. This is sort of what I want but not quite: What I need to be able to do is to reset properties by writing a null value to them. The control that I would like is for every control to have a context menu called reset that, when reset is clicked, a null value will be written to the control.

I started looking through xaml and C# code to try to figure these things out, but I'm afraid it's a little over my head at the moment. I'd appreciate a little direction.

 

Thanks,

Bill

Coordinator
Jan 12, 2011 at 7:35 AM

hi Bill, I agree with you - Nullable types should not be automatically optional. I changed the code - now you must set the [Optional] attribute to get the checkbox.

Jan 14, 2011 at 7:53 PM

This change causes a problem with Enum types that are nullable. If you have an Enum? property that is null, Convert in EnumValuesConverter will throw an exception.  I fixed this easily enough in the code below, but the Combo box Items for the Enum property are empty. I'm not exactly sure how to fix that, but I'll keep looking.

Also, for nullable properties, I think we need a way to reset (or make null) nullable properties that are not marked Optional. In my previous property editor, I used a Context menu with a Reset selection for controls bound to nullable properties. You may have a better idea.

 

Thanks,

Bill

    public class EnumValuesConverter : IValueConverter
    {
        #region IValueConverter Members

        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (value != null)
                return Enum.GetValues(value.GetType());
            else
                return value;

            
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotSupportedException();
        }

        #endregion
    }
}

Jan 14, 2011 at 8:38 PM

My WPF ability is not where yours is, but I think I can see one way to do this, maybe: The ItemsSource could be set in FindDataTemplate after you get the propertyType. There must be someway to get at the ComboBox and RadioButtons ItemsSource after you find the Template. Then you'd get rid of the ItemsSource Binding in the xaml.

Bill 

Jan 14, 2011 at 10:04 PM

As for the reset feature. I'm thinking of something like a Reset attribute, similar to the Optional attribute. There would then be a ResetPropertyViewModel with a DataTemplate that puts a small Reset button to the left of the property control.

If this sounds like the right thing to do, let me know if you want me to code it and I'll get Mercurial setup (which has been on my ToDo list for a while now anyway). It looks like I should mostly be able to follow your Optional attribute as an example, but I might need a little hand holding.

I think for now, the reset function will always write a null value to the property, but it might be better to have the option of the model providing an Interface, I'm not sure.

 

Thanks,

Bill

Coordinator
Jan 15, 2011 at 5:04 PM

That's a good idea, it could be ResettableAttribute with the reset value as an argument. Let us add it to the issue tracker as a new feature.

PropertyEditor is using Subversion, but I will consider to change to Mercurial if more developers are getting involved.

Developer
Jan 16, 2011 at 2:28 PM

No, please stay in Subversion.

Jan 17, 2011 at 1:36 PM

Then would anybody mind if I went ahead and coded this and submitted my changes via discussion? I'm stuck with VSS in-house, and I'll need to get Mercurial setup for collaboration on one project, but I'm way too busy to have to set-up, learn, teach, and maintain yet-another version control environment.

Jan 17, 2011 at 4:04 PM
Edited Jan 19, 2011 at 6:51 PM

I changed the title of this thread.

Assuming the answer to my question is "NO", I've included my changes for this change below. I mostly followed the OptionalAttribute as an example. Note than I continue to edit this post and the listed changes as the code evolves.

 

Thanks,

Bill

 

PropertyEditorTemplates.xaml Changes (see 1/19 posrt below, I'm still working on this):

    <!-- Template for the resettable property -->
    <DataTemplate DataType="{x:Type local:ResettablePropertyViewModel}">
        <DockPanel Margin="0 2 0 2" IsEnabled="{Binding IsEnabled}" Visibility="{Binding Visibility}">
            <Grid MinWidth="{Binding LabelWidth,RelativeSource={RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type local:PropertyEditor}}}">
                <Button Name="ResetButton" Content="{Binding Label, Mode=OneTime}" Command="{Binding ResetCommand}" ToolTip="{Binding ToolTip, Mode=OneTime}" Margin="5 0 5 0" HorizontalAlignment="{Binding LabelAlignment, RelativeSource={RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type local:PropertyEditor}}}" VerticalAlignment="Center" />
            </Grid>

            <Label MinWidth="{Binding LabelWidth,RelativeSource={RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type local:PropertyEditor}}}" Content="{Binding Header, Mode=OneTime}" ToolTip="{Binding ToolTip, Mode=OneTime}" HorizontalContentAlignment="{Binding LabelAlignment, RelativeSource={RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type local:PropertyEditor}}}" />

            <Border x:Name="border1" BorderThickness="{Binding ErrorBorderThickness, Mode=OneTime, RelativeSource={RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type local:PropertyEditor}}}">
                <ContentControl x:Name="content1" Content="{Binding}" ToolTip="{Binding ToolTip, Mode=OneTime}" Focusable="False" ContentTemplateSelector="{Binding PropertyTemplateSelector, Mode=OneTime}" />
            </Border>
        </DockPanel>
        <DataTemplate.Triggers>
            <DataTrigger Binding="{Binding PropertyWarning, Converter={StaticResource NullToBoolConverter}}" Value="False">
                <Setter TargetName="border1" Property="BorderBrush" Value="{StaticResource WarningBrush}" />
                <Setter TargetName="content1" Property="ToolTip" Value="{Binding PropertyWarning}" />
            </DataTrigger>
            <DataTrigger Binding="{Binding PropertyError, Converter={StaticResource NullToBoolConverter}}" Value="False">
                <Setter TargetName="border1" Property="BorderBrush" Value="{StaticResource ErrorBrush}" />
                <Setter TargetName="content1" Property="ToolTip" Value="{Binding PropertyError}" />
            </DataTrigger>
        </DataTemplate.Triggers>
    </DataTemplate>

DefaultPropertyViewModelFactory.cs Changes:

        public virtual bool IsResettable(PropertyDescriptor descriptor) {
            var oa = AttributeHelper.GetFirstAttribute<ResettableAttribute>(descriptor);
            if (oa != null) 
return true; return false; }
        public virtual PropertyViewModel CreateViewModel(object instance, PropertyDescriptor descriptor)
        {

     ...
            if (IsResettable(descriptor))
                propertyViewModel = new ResettablePropertyViewModel(instance, descriptor, owner);

     ...

Add ResettablePropertyViewModel.cs:

using System;
using System.ComponentModel;
using System.Windows.Input;

namespace PropertyEditorLibrary
{
    /// <summary>
    /// Properties that are marked [resettable(...)] have a reset button
    /// </summary>
    public class ResettablePropertyViewModel : PropertyViewModel
    {
        private PropertyDescriptor resettableDescriptor;
        private object instance;
        private object defaultValue;

        public string ResettablePropertyName
        {
            get
            {
                if (resettableDescriptor != null)
                    return resettableDescriptor.Name;

                return null;
            }
        }

        public ResettablePropertyViewModel(object instance, PropertyDescriptor descriptor, PropertyEditor owner)
            : base(instance, descriptor, owner)
        {
            this.instance = instance;
            this.resettableDescriptor = descriptor;
            this.ResetCommand = new DelegateCommand(ExecuteReset);

            var resettableAttr = AttributeHelper.GetFirstAttribute<ResettableAttribute>(resettableDescriptor);

            if (resettableAttr != null) {
                this.defaultValue = resettableAttr.DefaultValue;
                this.Label = (string)resettableAttr.ButtonLabel;
            }
        }

        public string Label { get; set; }

        /// <summary>
        /// Gets or sets BrowseCommand.
        /// </summary>
        public ICommand ResetCommand { get; set; }

        public void ExecuteReset() {
            IResettableProperty reset = this.instance as IResettableProperty;

            if (reset != null) reset.PropertyReset(this.resettableDescriptor.Name, this.defaultValue);
        }
    }
}

Add ResettableAttribute.cs:

using System;

namespace PropertyEditorLibrary
{
    /// <summary>
    /// The Resettable attribute is used for resettable properties.
    /// Properties marked with [Resettable] will have a button to the left of the lable.
    /// The button will reset the property to some configured reset value.
    /// Example usage:
    ///   [Resettable]          // reset value is null
    ///   [Resettable("ButtonLabel")] // reset value is Default (null)
    ///   [Resettable(0)] // reset value is 0, ButtonLabel = "Reset"
    /// </summary>
    [AttributeUsage(AttributeTargets.Property)]
    public class ResettableAttribute : Attribute
    {
        public static readonly ResettableAttribute Default;

        public object DefaultValue { get; set; }
        public object ButtonLabel { get; set; }

        public ResettableAttribute() {
            this.DefaultValue = null;
            this.ButtonLabel = "Reset";
        }

        public ResettableAttribute(string label) {
            this.DefaultValue = null;
            this.ButtonLabel = label;
        }

        public ResettableAttribute(int value) {
            this.DefaultValue = value;
            this.ButtonLabel = "Reset";
        }
    }
}


Added IResettableProperty.cs:

using System;

namespace PropertyEditorLibrary
{
    public interface IResettableProperty
    {
        void PropertyReset(string propertyName, object value);
    }
}

Jan 18, 2011 at 6:02 PM
Edited Jan 19, 2011 at 6:04 PM

I made some changes to ResettableAttribute.cs; updated previous post:

 

Jan 18, 2011 at 10:34 PM
Edited Jan 19, 2011 at 6:18 PM

I'm getting a little closer. I need to figure out how to format the DataTemplate so the Button shows up in some logical place.

I updated the source listings in my previous posts so it's all in one place as much as possible.

 

Bill

Jan 19, 2011 at 5:44 PM
Edited Jan 19, 2011 at 6:19 PM

I made some edits to some of my previous posts.

 

Bill

Coordinator
Jan 19, 2011 at 6:53 PM

hi Bill, great work! I think you should install TortoiseSVN and/or AnkhSVN and make a patch for your changes. That will make it easier for me to review!

You should look at the templates in the PropertyEditorTemplates.xaml. I think the templates for the WidePropertyViewModel or the OptionalPropertyViewModel could be good starting points.

Or add the reset button to the PropertyViewModel template and bind its Visibility property.

The reset button could be placed next to the ContentControl.

Jan 19, 2011 at 7:28 PM

OK. I think TortoiseSVN works with Mercurial, so I guess I can justify that.

I'll take a look at your suggestions.

I hadn't thought of adding it to the PropertyViewModel template and using the Visibility.

My initial thought is to try to minimize the number of references to the Resettable attribute, and at this point, PropertyViewModel has none.

 

Thanks,

 

Bill

Jan 20, 2011 at 6:16 PM

Just uploaded patch.

Please see if you can figure out the following:

  1. In EnumValuesConverter when value is null, still need to see a list of enums. My code doesn't work, but at least there is no exception when value is null. I think you can see what I was trying to do.
  2. In PropertyEditorTemplates.xaml, my changes for the ResettablePropertyViewModel DataTemplate
    1. The Button width seems not to be changeable
    2. The ContentControl does not stretch to fill the area before the button

I wasn't able to glean anything from the examples that you suggested I take a look at.  Sorry.

 

Thanks,

Bill

Coordinator
Feb 8, 2011 at 7:02 AM

hi Bill, I fixed the template by adding the Button before the Border/ContentControl and setting DockPanel.Dock=Right.

I modified the interface to


public interface IResettableProperties    {       
	object GetResetValue(string propertyName);
}

and removed the default value from the attribute (cannot set it there since a constant expression is required).

Have not found a solution to nullable enums yet...

Feb 14, 2011 at 3:05 PM

OK, thanks.

My project is on hold while I work on some other pressing issues.

Hopefully, when I get back to it, you will have had time to look into some of the things I cannot figure out.

 

Bill