Difference between revisions of "Implementing a data converter component"

From OPC Labs Knowledge Base
Jump to navigation Jump to search
Line 508: Line 508:
  
 
You can see that the work is delegated to the {{Style=Identifier|ConvertToColor}} method, which tests the input value for various supported types, and returns a corresponding color based on rules that are in effect for this component.
 
You can see that the work is delegated to the {{Style=Identifier|ConvertToColor}} method, which tests the input value for various supported types, and returns a corresponding color based on rules that are in effect for this component.
 +
 +
{{Note|The pairs of ResetXXXX and ShouldSerializeXXXX methods can probably be removed and replaced simply by a [DefaultValue(...)] attribute placed on the corresponding properties. Also, the serialization support may be unnecessary. The Equals2 and GetHashCode2 methods have to do with inheritance from our Widget class, but you wouldn't need them if you inherit from Component.}}

Revision as of 13:02, 14 December 2016


It may sometimes come handy to develop a custom data converter for use in Live Binding. The main requirement on such data converter is that it must implement the IDataConverter interface. This interface has two methods: Convert (essentially, used when Live Binding makes a Read, or provides data from a subscription) and ConvertBack (essentially, used when Live Binding is doing a Write). You may implement just one of them, if the other direction is not needed.

If you want to use the data converter as other components in Windows Forms, it also needs to inherit (directly or indirectly) from Component, and fulfill requirements related to it. This is a large area that is out of scope of this article.

The code below is a redacted version of the StatusToColor data converter that is included with Live Binding (version: QuickOPC 2016.2).

using System;
using System.ComponentModel;
using System.Drawing;
using System.Globalization;
using System.Runtime.Serialization;
using System.Security;
using OpcLabs.BaseLib.Data;
using OpcLabs.BaseLib.Extensions;
using OpcLabs.BaseLib.Runtime.Serialization.Extensions;
using OpcLabs.BaseLib.Utilities;
using OpcLabs.BaseLib.Widgets;

namespace OpcLabs.BaseLib.Components
{
    /// <summary>
    /// Assigns different colors based on status information.
    /// </summary>
    [Category("Data Converters")]
    [Description("Assigns different colors based on status information.")] // used e.g. in Toolbox
    [Serializable]
    [ToolboxBitmap(typeof(StatusToColorConverter))]
    public sealed class StatusToColorConverter
        : Widget
        , ICloneable
        , IDataConverter
    {
#region Public Constructors

        /// <overloads>
        /// <summary>
        /// Initializes an instance of the <see cref="StatusToColorConverter"/> class. 
        /// </summary>
        /// </overloads>
        public StatusToColorConverter()
        {
            Construct();
        }

        /// <param name="unknownColor">The color returned for Unknown status.</param>
        /// <param name="normalColor">The color returned for Normal status.</param>
        /// <param name="warningColor">The color returned for Warning status.</param>
        /// <param name="errorColor">The color returned for Error status.</param>
        public StatusToColorConverter(Color unknownColor, Color normalColor, Color warningColor, Color errorColor)
            : this()
        {
            UnknownColor = unknownColor;
            NormalColor = normalColor;
            WarningColor = warningColor;
            ErrorColor = errorColor;
        }

        /// <summary>
        /// Initializes an instance of the <see cref="StatusToColorConverter"/> class with Designer support. 
        /// </summary>
        /// <param name="container">The component container to which the new instance will be added.</param>
        public StatusToColorConverter(IContainer container)
        {
            Construct();
            container.Add(this);    // place it *after* the Construct, as the VS designer can call us from here
        }

        /// <summary>Initializes a new instance of the class, copying values from a given object.</summary>
        /// <param name="statusToColorConverter">The object to be copied from.</param>
        public StatusToColorConverter(StatusToColorConverter statusToColorConverter)
            : base(statusToColorConverter)
        {
            UnknownColor = statusToColorConverter.UnknownColor;
            NormalColor = statusToColorConverter.NormalColor;
            WarningColor = statusToColorConverter.WarningColor;
            ErrorColor = statusToColorConverter.ErrorColor;
        }

#endregion



#region Private Constructors

        /// <summary>Initializes a new instance of the class with serialized data.</summary>
        /// <param name="info">The SerializationInfo that holds the serialized object data.</param>
        /// <param name="context">The StreamingContext that contains contextual information about the source or destination. 
        /// </param>
        private StatusToColorConverter(SerializationInfo info, StreamingContext context)
            : base(info, context)
        {
            UnknownColor = info.GetObject<Color>("UnknownColor");
            NormalColor = info.GetObject<Color>("NormalColor");
            WarningColor = info.GetObject<Color>("WarningColor");
            ErrorColor = info.GetObject<Color>("ErrorColor");
        }

#endregion



#region Static Public Properties

        /// <summary>
        /// The default Error color.
        /// </summary>
        static public Color DefaultErrorColor => Color.Red;

        /// <summary>
        /// The default Normal color.
        /// </summary>
        static public Color DefaultNormalColor => Color.White;

        /// <summary>
        /// The default Unknown color.
        /// </summary>
        static public Color DefaultUnknownColor => Color.Gray;

        /// <summary>
        /// The default Warning color.
        /// </summary>
        static public Color DefaultWarningColor => Color.Yellow;

#endregion



#region Static Public Methods

        /// <summary>Determines whether the two objects are equal.</summary>
        /// <param name="left">First object to be compared.</param>
        /// <param name="right">Second object to be compared.</param>
        /// <returns><c>True</c> if the objects are equal; <c>false</c> otherwise.</returns>
        static public bool operator ==(StatusToColorConverter left, StatusToColorConverter right)
        {
            return Equals(left, right);
        }

        /// <summary>Determines whether the two objects are not equal.</summary>
        /// <param name="left">First object to be compared.</param>
        /// <param name="right">Second object to be compared.</param>
        /// <returns><c>True</c> if the objects are not equal; <c>false</c> if they are equal.</returns>
        static public bool operator !=(StatusToColorConverter left, StatusToColorConverter right)
        {
            return !Equals(left, right);
        }

#endregion



#region Public Properties

        /// <summary>
        /// The color returned for Error status.
        /// </summary>
        [Description("The color returned for Error status.")]
        public Color ErrorColor { get; set; }

        /// <summary>
        /// The color returned for Normal status.
        /// </summary>
        [Description("The color returned for Normal status.")]
        public Color NormalColor { get; set; }

        /// <summary>
        /// The color returned for Unknown status.
        /// </summary>
        [Description("The color returned for Unknown status.")]
        public Color UnknownColor { get; set; }

        /// <summary>
        /// The color returned for Warning status.
        /// </summary>
        [Description("The color returned for Warning status.")]
        public Color WarningColor { get; set; }

#endregion



#region Public Methods

        /// <summary>
        /// Determines whether the specified <see cref="StatusToColorConverter"/> is equal to the current object.
        /// </summary>
        /// <param name="other">The <see cref="StatusToColorConverter"/> to compare with the current object.</param>
        /// <returns><c>true</c> if the specified object is equal to the current object; otherwise, <c>false</c>.</returns>
        public bool Equals(StatusToColorConverter other)
        {
            if (other == null)
                return false;
            return
                base.Equals(other) &&
                ErrorColor.Equals(other.ErrorColor) &&
                NormalColor.Equals(other.NormalColor) &&
                UnknownColor.Equals(other.UnknownColor) &&
                WarningColor.Equals(other.WarningColor);
        }

#endregion



#region ICloneable Members

        object ICloneable.Clone()
        {
            return Clone();
        }

#endregion



#region IDataConverter Members

        /// <inheritdoc cref="IDataConverter.Convert"/>
        /// <exception cref="ArgumentException">When the value is not of any of the supported types.</exception>
        /// <remarks>
        /// Values are converted to colors according to following rules:
        /// <ul>
        ///     <li><c>null</c> converts to <see cref="NormalColor"/>.</li>
        ///     <li>A <see cref="StatusInfo.Normal"/> converts to <see cref="NormalColor"/>, 
        ///         a <see cref="StatusInfo.Warning"/> converts to <see cref="WarningColor"/>,
        ///         a <see cref="StatusInfo.Error"/> converts to <see cref="ErrorColor"/>, and
        ///         a <see cref="StatusInfo.Unknown"/> converts to <see cref="UnknownColor"/>.</li>
        ///     <li>An <see cref="Exception"/>-typed value converts to <see cref="WarningColor"/>.</li>
        ///     <li>Empty strings convert to <see cref="NormalColor"/>, 
        ///         non-empty strings convert to <see cref="ErrorColor"/>.</li>
        ///     <li>Zero integers convert to <see cref="NormalColor"/>, 
        ///         positive integers convert to <see cref="WarningColor"/>,
        ///         negative integers convert to <see cref="ErrorColor"/>.</li>
        ///     <li>A <c>true</c> Boolean converts to <see cref="NormalColor"/>,
        ///         a <c>false</c> Boolean converts to <see cref="ErrorColor"/>.</li>
        /// </ul>
        /// </remarks>
        public object Convert(
            object value, object parameter, CultureInfo culture)
        {
            Color color = ConvertToColor(value);
            return color;
        }

        /// <inheritdoc cref="IDataConverter.ConvertBack"/>
        /// <exception cref="NotSupportedException">Thrown.</exception>
        /// <remarks>Conversion in this direction is not supported.</remarks>
        public object ConvertBack(object value, object parameter, CultureInfo culture)
        {
            throw new NotSupportedException("Inverse conversion is not supported by StatusToColorConverter.");
        }

#endregion



#region ISerializable Members

        /// <inheritdoc cref="ISerializable.GetObjectData"/>
        [SecurityCritical]
        public override void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            base.GetObjectData(info, context);
            info.AddValue("UnknownColor", UnknownColor);
            info.AddValue("NormalColor", NormalColor);
            info.AddValue("WarningColor", WarningColor);
            info.AddValue("ErrorColor", ErrorColor);
        }

#endregion



#region Protected Methods

        /// <inheritdoc cref="Widget.Equals2"/>
        protected override bool Equals2(object obj)
        {
            if (ReferenceEquals(null, obj)) return false;
            if (ReferenceEquals(this, obj)) return true;
            if (obj.GetType() != /*this.*/GetType()) return false;
            return Equals((StatusToColorConverter)obj);
        }

        /// <inheritdoc cref="Widget.GetHashCode2"/>
        protected override int GetHashCode2()
        {
            unchecked
            {
                var hashCode = base.GetHashCode2();
                hashCode = (hashCode * 397) ^ ErrorColor.GetHashCode();
                hashCode = (hashCode * 397) ^ NormalColor.GetHashCode();
                hashCode = (hashCode * 397) ^ UnknownColor.GetHashCode();
                hashCode = (hashCode * 397) ^ WarningColor.GetHashCode();
                return hashCode;
            }
        }

#endregion



#region Private Methods

        /// <summary>
        /// Creates a new object that is a copy of the current instance.
        /// </summary>
        /// <returns>A new object that is a copy of this instance.</returns>
        private StatusToColorConverter Clone()
        {
            return new StatusToColorConverter(this);
        }

        private void Construct()
        {
            ResetErrorColor();
            ResetNormalColor();
            ResetUnknownColor();
            ResetWarningColor();
        }

        private Color ConvertToColor(object value)
        {
            if (value == null)
                return NormalColor; // this is actually required so that we can test against Exception properties

            {
                if (value is StatusInfo)
                    return DetermineColor((StatusInfo)value);
            }

            {
                var exception = value as Exception;
                if (exception != null)
                    return DetermineColor(exception);
            }

            {
                var s = value as string;
                if (s != null)
                    return DetermineColor(s);
            }

            {
                if (value is Byte)
                    return DetermineColor(Math.Sign((Byte) value));
                if (value is SByte)
                    return DetermineColor(Math.Sign((SByte)value));
                if (value is UInt16)
                    return DetermineColor(Math.Sign((UInt16)value));
                if (value is Int16)
                    return DetermineColor(Math.Sign((Int16)value));
                if (value is UInt32)
                    return DetermineColor(Math.Sign((UInt32)value));
                if (value is Int32)
                    return DetermineColor(Math.Sign((Int32)value));
                if (value is UInt64)
                    return DetermineColor(MathUtilities.Sign((UInt64)value));
                if (value is Int64)
                    return DetermineColor(Math.Sign((Int64)value));

                if (value is UIntPtr)
                    return DetermineColor(MathUtilities.Sign((UIntPtr)value));
                if (value is IntPtr)
                    return DetermineColor(MathUtilities.Sign((IntPtr)value));
            }

            {
                if (value is bool)
                    return DetermineColor((bool) value);
            }

            throw new ArgumentException(
                String.Format(CultureInfo.CurrentCulture,
                    "We do not know how to convert a status of type '{0}' to a color.",
                    value.GetType()));
        }

        private Color DetermineColor(bool b)
        {
            return b ? NormalColor : ErrorColor;
        }

        private Color DetermineColor(
            Exception exception)
        {
            return ErrorColor;
        }

        private Color DetermineColor(int sign)
        {
            switch (sign)
            {
                case -1:
                    return ErrorColor;
                case 0:
                    return NormalColor;
                case +1:
                    return WarningColor;
            }
            return UnknownColor;
        }

        private Color DetermineColor(StatusInfo statusInfo)
        {
            switch (statusInfo)
            {
                case StatusInfo.Normal:
                    return NormalColor;
                case StatusInfo.Warning:
                    return WarningColor;
                case StatusInfo.Error:
                    return ErrorColor;
            }
            return UnknownColor;
        }

        private Color DetermineColor(string s)
        {
            return DetermineColor(s.IsEmpty());
        }

        /// <summary>
        /// Reset the <see cref="ErrorColor"/> property to its default value.
        /// </summary>
        // The method name is of the essence - for the designer, it must match the name of the property
        private void ResetErrorColor()
        {
            ErrorColor = DefaultErrorColor;
        }

        /// <summary>
        /// Reset the <see cref="NormalColor"/> property to its default value.
        /// </summary>
        // The method name is of the essence - for the designer, it must match the name of the property
        private void ResetNormalColor()
        {
            NormalColor = DefaultNormalColor;
        }

        /// <summary>
        /// Reset the <see cref="UnknownColor"/> property to its default value.
        /// </summary>
        // The method name is of the essence - for the designer, it must match the name of the property
        private void ResetUnknownColor()
        {
            UnknownColor = DefaultUnknownColor;
        }

        /// <summary>
        /// Reset the <see cref="WarningColor"/> property to its default value.
        /// </summary>
        // The method name is of the essence - for the designer, it must match the name of the property
        private void ResetWarningColor()
        {
            WarningColor = DefaultWarningColor;
        }

        /// <summary>
        /// Determines whether the <see cref="ErrorColor"/> property is equal to its default value.
        /// </summary>
        /// <returns><c>true</c> if the property has changed from its default value; <c>false</c> otherwise.</returns>
        // The method name is of the essence - for the designer, it must match the name of the property
        private bool ShouldSerializeErrorColor()
        {
            return ErrorColor != DefaultErrorColor;
        }

        /// <summary>
        /// Determines whether the <see cref="NormalColor"/> property is equal to its default value.
        /// </summary>
        /// <returns><c>true</c> if the property has changed from its default value; <c>false</c> otherwise.</returns>
        // The method name is of the essence - for the designer, it must match the name of the property
        private bool ShouldSerializeNormalColor()
        {
            return NormalColor != DefaultNormalColor;
        }

        /// <summary>
        /// Determines whether the <see cref="UnknownColor"/> property is equal to its default value.
        /// </summary>
        /// <returns><c>true</c> if the property has changed from its default value; <c>false</c> otherwise.</returns>
        // The method name is of the essence - for the designer, it must match the name of the property
        private bool ShouldSerializeUnknownColor()
        {
            return UnknownColor != DefaultUnknownColor;
        }

        /// <summary>
        /// Determines whether the <see cref="WarningColor"/> property is equal to its default value.
        /// </summary>
        /// <returns><c>true</c> if the property has changed from its default value; <c>false</c> otherwise.</returns>
        // The method name is of the essence - for the designer, it must match the name of the property
        private bool ShouldSerializeWarningColor()
        {
            return WarningColor != DefaultWarningColor;
        }

#endregion
    }
}

You can see that the work is delegated to the ConvertToColor method, which tests the input value for various supported types, and returns a corresponding color based on rules that are in effect for this component.

Note2-icon.png

Note: The pairs of ResetXXXX and ShouldSerializeXXXX methods can probably be removed and replaced simply by a [DefaultValue(...)] attribute placed on the corresponding properties. Also, the serialization support may be unnecessary. The Equals2 and GetHashCode2 methods have to do with inheritance from our Widget class, but you wouldn't need them if you inherit from Component.