Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeConverter implementation for Units #189

Closed
celsound opened this issue Sep 30, 2016 · 13 comments
Closed

TypeConverter implementation for Units #189

celsound opened this issue Sep 30, 2016 · 13 comments

Comments

@celsound
Copy link

Hi,

One feature I've found myself wanting in UnitsNet is the ability to edit unit types in a PropertyGrid. As of now, the property grid will display the ToString() method of the unit (which works great). However, the Unit properties may not be edited.

(See http://csharphelper.com/blog/2014/09/use-a-type-converter-with-a-propertygrid-control-in-c/)

Something like this is probably really easy to implement using the TryParse methods already present in UnitsNet.

@angularsen
Copy link
Owner

Hi, I'm not familiar with this myself , but I think this should be safe to add and provide some value to users.
I suspected that type converters do not work well with immutable structs, which a quick google seemed to confirm: http://stackoverflow.com/a/20895375/134761

I don't know if this is a showstopper or if there are workarounds, just a heads up to you for something to look into.

Let me know if you want to do a pull request, I'll merge it in if you get it working.

@angularsen
Copy link
Owner

Closing, will reopen if there are any further discussions.

@celsound
Copy link
Author

Hey,

I will have to implement this for my project eventually, so I will let you know how I end up solving the issue. For now keep it closed.

Cheers,
Celso

@chromhelm
Copy link
Contributor

chromhelm commented Mar 15, 2019

Hi
I have made a small implementation

    class UnitsNETTypeConverter<T> : TypeConverter where T : IQuantity
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
        }

        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
        {
            string stringValue;
            object result;

            result = null;
            stringValue = value as string;

            if (!string.IsNullOrEmpty(stringValue))
            {
                stringValue = stringValue.Replace("^1", "");
                stringValue = stringValue.Replace("^2", "²");
                stringValue = stringValue.Replace("^3", "³");
                stringValue = stringValue.Replace("^4", "⁴");
                result = Quantity.Parse(typeof(T), stringValue);
            }

            return result ?? base.ConvertFrom(context, culture, value);
        }
    }

It wold be nice if the parse function itself could substitute the exponents.

@chromhelm
Copy link
Contributor

Hi
Her an updated version
I added the possibility to set default unit if it is not typed by the user.
I also added the possibility to specify that the unit is always converted to a particular unit.

I took inspired for this work from this places:

https://stackoverflow.com/questions/14929681/how-to-pass-parameter-to-typeconverter-derived-class
https://stackoverflow.com/questions/14867793/enum-as-an-parameter-on-its-own-custom-attribute-constructor/14868343
https://www.cyotek.com/blog/creating-a-custom-typeconverter-part-1

    [AttributeUsage(AttributeTargets.Property , AllowMultiple = false, Inherited = true)]
    public class DefaultUnit : Attribute
    {
        public DefaultUnit(object unitType) { UnitType = unitType as Enum; }
        public Enum UnitType { get; set; }
    }
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
    public class ConvertToUnit : Attribute
    {
        public ConvertToUnit(object unitType) { UnitType = unitType as Enum; }
        public Enum UnitType { get; set; }
    }

    class UnitsNETTypeConverter<T> : TypeConverter where T : IQuantity
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
        }

        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
        {
            string stringValue;
            object result;
            AttributeCollection ua = null;

            result = null;
            stringValue = value as string;

            if (!string.IsNullOrEmpty(stringValue))
            {
                stringValue = stringValue.Replace("^1", "");
                stringValue = stringValue.Replace("^2", "²");
                stringValue = stringValue.Replace("^3", "³");
                stringValue = stringValue.Replace("^4", "⁴");

                if (context != null)
                {
                    ua = context.PropertyDescriptor.Attributes;
                }

                    double dvalue;
                if (double.TryParse(stringValue, out dvalue))
                {
                    DefaultUnit defaultUnit = null;
                    if (ua != null)
                    {
                        defaultUnit = (DefaultUnit)ua[typeof(DefaultUnit)];
                    }
                    else
                    {
                        defaultUnit = new DefaultUnit(default(T).Unit);
                    }

                    result = Quantity.From(dvalue, defaultUnit.UnitType);
                }
                else
                {
                    result = Quantity.Parse(typeof(T), stringValue);
                }

                if (ua != null)
                {
                    ConvertToUnit convertToUnit = (ConvertToUnit)ua[typeof(ConvertToUnit)];
                    if(convertToUnit != null)
                    {
                        result = (result as IQuantity).ToUnit(convertToUnit.UnitType);
                    }
                }
            }

            return result ?? base.ConvertFrom(context, culture, value);
        }
    }

@angularsen
Copy link
Owner

Thanks for posting @chromhelm , would you care to do a pull request on this? Then we can give a proper review of the code too.

It seems like TypeConverter is supported in netstandard2.0 so I think that should work.
If you give it a go, please also add some unit tests on the public members of the converter implementation.

@angularsen
Copy link
Owner

Fixed in #644

@wenndemann
Copy link

Hi,
I had a closer look to the ability to change the default unit of a quantity. But I can't figure out how to change that. I like to change i.e. the default unit of RotationalSpeed to RevolutionPerMinute. Can you give me an example how to do this from my app?

@angularsen
Copy link
Owner

@wenndemann It depends on what you mean by default unit of a quantity.
new RotationalSpeed(1, RotationalSpeedUnit.RevolutionPerMinute) and RotationalSpeed.FromRevolutionsPerMinute(1) all specify the unit explicitly.

The exception is new RotationalSpeed(1), which uses BaseUnit property as default unit. This cannot be changed today.

There is some work in progress for v5 in #709, but work has not continued there for some time now.

@angularsen
Copy link
Owner

@wenndemann Oh, my bad, I forgot you referred to type converters as in #644 .

There is a new DefaultUnitAttribute that you can use. I haven't tried it myself, but I assume you add these attributes like so:

[DefaultUnit(LengthUnit.Centimeter)]
public Length MyLength {get;set;}

It then affects how that property is rendered in a grid view or other Winforms/WPF GUI components. I am speculating tho, it's best if @chromhelm gives an example maybe. Then we can add it to the wiki.

@lipchev
Copy link
Collaborator

lipchev commented Apr 5, 2020

Yes please- I'm also still a little confused as to what can be achieved by using the TypeConverter + DefaultUnitAttribute - is it possible to convert to another unit or is it just for binding to string? Can I use this somehow to expose a given quantity as a specified unit (inside a view model for instance)- such that the quantity is always converted to the specified unit (not interested in binding to a string value- I only work with quantities)

@lipchev
Copy link
Collaborator

lipchev commented Apr 5, 2020

I mean it would be awesome if we could have this test succeed:

    public class TestDataObject
    {
        [DefaultUnit(LengthUnit.Centimeter)]
        public Length MyLength { get; set; }
    }

        [Fact]
        public void DefaultUnitAttribute_GivenObjectWithCustomAnnotation_StoresValueInSpecifiedUnit()
        {
            var myObject = new TestDataObject();
            
            Assert.Equal(LengthUnit.Centimeter, myObject.MyLength.Unit);
        }

@wenndemann
Copy link

Well I use it to parse some old INI files into objects so wrote an ConfigurationExtension that gives a key/value pair as string. Unfortunately some of the values has no unit and so I got into these problems. I derive the QuantityTypeConverter and create a context for the conversion

public class UnitsNetConverter<T> : QuantityTypeConverter<T>
    where T: IQuantity, new()
{
    public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
    {
        if(context == null)
        {
            // get all attributes of this quantity type
            AttributeCollection attrCol = TypeDescriptor.GetAttributes(typeof(T));
            Attribute[] attr = new Attribute[attrCol.Count];
            attrCol.CopyTo(attr, 0);

            // create a new context
            context = new TypeDescriptorContext("SomeMemberName", attr);
        }

        return base.ConvertFrom(context, culture, value);
    }
}

Now I just need to add the TypeConverterAttribute and the DefaultUnitAttribute for a Quantity type and it uses the default unit I like.

    TypeDescriptor.AddAttributes(typeof(RotationalSpeed), new Attribute[]
    {
        new TypeConverterAttribute(typeof(UnitsNetConverter<RotationalSpeed>)), 
        new DefaultUnitAttribute(UnitsNet.Units.RotationalSpeedUnit.RevolutionPerMinute),
    });

    RotationalSpeed rs = (RotationalSpeed)(TypeDescriptor.GetConverter(typeof(RotationalSpeed)).ConvertFrom("1")); // 1 rpm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants