# HG changeset patch
# User Stephane Lenclud
# Date 1471619574 -7200
# Node ID cc2251d065db413d4792614d059deb84ad9b006e
# Parent  0a956121c273aab09d1b25b368b5495947f8da9d
Optical drive eject action now functional.

diff -r 0a956121c273 -r cc2251d065db Server/FormEditObject.cs
--- a/Server/FormEditObject.cs	Thu Aug 18 20:14:30 2016 +0200
+++ b/Server/FormEditObject.cs	Fri Aug 19 17:12:54 2016 +0200
@@ -104,22 +104,25 @@
         private void FetchPropertiesValue(T aObject)
         {
             int ctrlIndex = 0;
+            //For each of our properties
             foreach (PropertyInfo pi in aObject.GetType().GetProperties())
             {
-                AttributeObjectProperty[] attributes =
-                    ((AttributeObjectProperty[]) pi.GetCustomAttributes(typeof(AttributeObjectProperty), true));
+                //Get our property attribute
+                AttributeObjectProperty[] attributes = ((AttributeObjectProperty[]) pi.GetCustomAttributes(typeof(AttributeObjectProperty), true));
                 if (attributes.Length != 1)
                 {
+                    //No attribute, skip this property then.
                     continue;
                 }
-
                 AttributeObjectProperty attribute = attributes[0];
 
+                //Check that we support this type of property
                 if (!IsPropertyTypeSupported(pi))
                 {
                     continue;
                 }
 
+                //Now fetch our property value
                 GetPropertyValueFromControl(iTableLayoutPanel.Controls[ctrlIndex+1], pi, aObject); //+1 otherwise we get the label
 
                 ctrlIndex+=2; //Jump over the label too
@@ -163,6 +166,14 @@
                 PropertyFile value = new PropertyFile {FullPath=ctrl.Text};
                 aInfo.SetValue(aObject, value);
             }
+            else if (aInfo.PropertyType == typeof(PropertyComboBox))
+            {
+                ComboBox ctrl = (ComboBox)aControl;
+                string currentItem = ctrl.SelectedItem.ToString();
+                PropertyComboBox pcb = (PropertyComboBox)aInfo.GetValue(aObject);
+                pcb.CurrentItem = currentItem;
+            }
+
             //TODO: add support for other types here
         }
 
@@ -191,21 +202,21 @@
             {
                 //Enum properties are using combo box
                 ComboBox ctrl = new ComboBox();
-                ctrl.AutoSize = true;                
-                ctrl.Sorted = true;                
+                ctrl.AutoSize = true;
+                ctrl.Sorted = true;
                 ctrl.DropDownStyle = ComboBoxStyle.DropDownList;
                 //Data source is fine but it gives us duplicate entries for duplicated enum values
                 //ctrl.DataSource = Enum.GetValues(aInfo.PropertyType);
 
                 //Therefore we need to explicitly create our items
-                Size cbSize = new Size(0,0);
+                Size cbSize = new Size(0, 0);
                 foreach (string name in aInfo.PropertyType.GetEnumNames())
                 {
                     ctrl.Items.Add(name.ToString());
                     Graphics g = this.CreateGraphics();
                     //Since combobox autosize would not work we need to get measure text ourselves
-                    SizeF size=g.MeasureString(name.ToString(), ctrl.Font);
-                    cbSize.Width = Math.Max(cbSize.Width,(int)size.Width);
+                    SizeF size = g.MeasureString(name.ToString(), ctrl.Font);
+                    cbSize.Width = Math.Max(cbSize.Width, (int)size.Width);
                     cbSize.Height = Math.Max(cbSize.Height, (int)size.Height);
                 }
 
@@ -225,7 +236,7 @@
                 CheckBox ctrl = new CheckBox();
                 ctrl.AutoSize = true;
                 ctrl.Text = aAttribute.Description;
-                ctrl.Checked = (bool)aInfo.GetValue(aObject);                
+                ctrl.Checked = (bool)aInfo.GetValue(aObject);
                 return ctrl;
             }
             else if (aInfo.PropertyType == typeof(string))
@@ -263,8 +274,27 @@
 
                 return ctrl;
             }
+            else if (aInfo.PropertyType == typeof(PropertyComboBox))
+            {
+                //ComboBox property
+                ComboBox ctrl = new ComboBox();
+                ctrl.AutoSize = true;
+                ctrl.Sorted = true;
+                ctrl.DropDownStyle = ComboBoxStyle.DropDownList;
+                //Data source is such a pain to set the current item
+                //ctrl.DataSource = ((PropertyComboBox)aInfo.GetValue(aObject)).Items;                
+
+                PropertyComboBox pcb = ((PropertyComboBox)aInfo.GetValue(aObject));
+                foreach (string item in pcb.Items)
+                {
+                    ctrl.Items.Add(item);
+                }
+
+                ctrl.SelectedItem = ((PropertyComboBox)aInfo.GetValue(aObject)).CurrentItem;
+                //
+                return ctrl;
+            }
             //TODO: add support for other control type here
-
             return null;
         }
 
@@ -294,6 +324,11 @@
             {
                 return true;
             }
+            else if (aInfo.PropertyType == typeof(PropertyComboBox))
+            {
+                return true;
+            }
+
             //TODO: add support for other type here
 
             return false;
diff -r 0a956121c273 -r cc2251d065db Server/FormMain.Designer.cs
--- a/Server/FormMain.Designer.cs	Thu Aug 18 20:14:30 2016 +0200
+++ b/Server/FormMain.Designer.cs	Fri Aug 19 17:12:54 2016 +0200
@@ -247,7 +247,7 @@
             this.toolStripStatusLabelSpring,
             this.toolStripStatusLabelPower,
             this.toolStripStatusLabelFps});
-            this.statusStrip.Location = new System.Drawing.Point(0, 539);
+            this.statusStrip.Location = new System.Drawing.Point(0, 540);
             this.statusStrip.Name = "statusStrip";
             this.statusStrip.RenderMode = System.Windows.Forms.ToolStripRenderMode.Professional;
             this.statusStrip.Size = new System.Drawing.Size(784, 22);
@@ -1124,7 +1124,7 @@
             // buttonEventEdit
             // 
             this.buttonEventEdit.Enabled = false;
-            this.buttonEventEdit.Location = new System.Drawing.Point(6, 35);
+            this.buttonEventEdit.Location = new System.Drawing.Point(6, 187);
             this.buttonEventEdit.Name = "buttonEventEdit";
             this.buttonEventEdit.Size = new System.Drawing.Size(96, 23);
             this.buttonEventEdit.TabIndex = 29;
@@ -1135,7 +1135,7 @@
             // buttonEventDelete
             // 
             this.buttonEventDelete.Enabled = false;
-            this.buttonEventDelete.Location = new System.Drawing.Point(6, 64);
+            this.buttonEventDelete.Location = new System.Drawing.Point(6, 216);
             this.buttonEventDelete.Name = "buttonEventDelete";
             this.buttonEventDelete.Size = new System.Drawing.Size(96, 23);
             this.buttonEventDelete.TabIndex = 28;
@@ -1145,7 +1145,7 @@
             // 
             // buttonEventAdd
             // 
-            this.buttonEventAdd.Location = new System.Drawing.Point(6, 6);
+            this.buttonEventAdd.Location = new System.Drawing.Point(6, 158);
             this.buttonEventAdd.Name = "buttonEventAdd";
             this.buttonEventAdd.Size = new System.Drawing.Size(96, 23);
             this.buttonEventAdd.TabIndex = 27;
@@ -1156,7 +1156,7 @@
             // buttonEventTest
             // 
             this.buttonEventTest.Enabled = false;
-            this.buttonEventTest.Location = new System.Drawing.Point(6, 93);
+            this.buttonEventTest.Location = new System.Drawing.Point(6, 245);
             this.buttonEventTest.Name = "buttonEventTest";
             this.buttonEventTest.Size = new System.Drawing.Size(96, 23);
             this.buttonEventTest.TabIndex = 26;
@@ -1167,7 +1167,7 @@
             // buttonActionEdit
             // 
             this.buttonActionEdit.Enabled = false;
-            this.buttonActionEdit.Location = new System.Drawing.Point(6, 190);
+            this.buttonActionEdit.Location = new System.Drawing.Point(6, 35);
             this.buttonActionEdit.Name = "buttonActionEdit";
             this.buttonActionEdit.Size = new System.Drawing.Size(96, 23);
             this.buttonActionEdit.TabIndex = 25;
@@ -1200,7 +1200,7 @@
             // buttonActionTest
             // 
             this.buttonActionTest.Enabled = false;
-            this.buttonActionTest.Location = new System.Drawing.Point(6, 248);
+            this.buttonActionTest.Location = new System.Drawing.Point(6, 93);
             this.buttonActionTest.Name = "buttonActionTest";
             this.buttonActionTest.Size = new System.Drawing.Size(96, 23);
             this.buttonActionTest.TabIndex = 22;
@@ -1211,7 +1211,7 @@
             // buttonActionDelete
             // 
             this.buttonActionDelete.Enabled = false;
-            this.buttonActionDelete.Location = new System.Drawing.Point(6, 219);
+            this.buttonActionDelete.Location = new System.Drawing.Point(6, 64);
             this.buttonActionDelete.Name = "buttonActionDelete";
             this.buttonActionDelete.Size = new System.Drawing.Size(96, 23);
             this.buttonActionDelete.TabIndex = 21;
@@ -1222,9 +1222,9 @@
             // buttonActionAdd
             // 
             this.buttonActionAdd.Enabled = false;
-            this.buttonActionAdd.Location = new System.Drawing.Point(6, 157);
+            this.buttonActionAdd.Location = new System.Drawing.Point(6, 6);
             this.buttonActionAdd.Name = "buttonActionAdd";
-            this.buttonActionAdd.Size = new System.Drawing.Size(96, 27);
+            this.buttonActionAdd.Size = new System.Drawing.Size(96, 23);
             this.buttonActionAdd.TabIndex = 20;
             this.buttonActionAdd.Text = "Add Action";
             this.buttonActionAdd.UseVisualStyleBackColor = true;
@@ -1370,7 +1370,7 @@
             // 
             this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
             this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
-            this.ClientSize = new System.Drawing.Size(784, 561);
+            this.ClientSize = new System.Drawing.Size(784, 562);
             this.Controls.Add(this.labelFontHeight);
             this.Controls.Add(this.labelFontWidth);
             this.Controls.Add(this.labelWarning);
diff -r 0a956121c273 -r cc2251d065db Server/FormMain.cs
--- a/Server/FormMain.cs	Thu Aug 18 20:14:30 2016 +0200
+++ b/Server/FormMain.cs	Fri Aug 19 17:12:54 2016 +0200
@@ -151,7 +151,7 @@
                 // We loaded events and actions from our settings
                 // Internalizer apparently skips constructor so we need to initialize it here
                 // Though I reckon that should only be needed when loading an empty EAR manager I guess.
-                Properties.Settings.Default.EarManager.Init();
+                Properties.Settings.Default.EarManager.Construct();
             }
             iSkipFrameRendering = false;
             iClosing = false;
diff -r 0a956121c273 -r cc2251d065db SharpLibEar/ActionOpticalDriveEject.cs
--- a/SharpLibEar/ActionOpticalDriveEject.cs	Thu Aug 18 20:14:30 2016 +0200
+++ b/SharpLibEar/ActionOpticalDriveEject.cs	Fri Aug 19 17:12:54 2016 +0200
@@ -1,12 +1,308 @@
-using System;
+using Microsoft.Win32.SafeHandles;
+using System;
 using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
 using System.Linq;
+using System.Runtime.Serialization;
 using System.Text;
 using System.Threading.Tasks;
+using SharpLib.Win32;
+using System.ComponentModel;
+using System.Runtime.InteropServices;
 
 namespace SharpLib.Ear
 {
-    class ActionOpticalDriveEject
+    [DataContract]
+    [AttributeObject(Id = "Action.OpticalDrive.Eject", Name = "Eject", Description = "Eject media from an optical drive.")]
+    public class ActionOpticalDriveEject : Action
     {
+        [DataMember]
+        [AttributeObjectProperty
+            (
+            Id = "Action.OpticalDrive.Eject.Drive",
+            Name = "Drive to eject",
+            Description = "Select the drive you want to eject."
+            )
+        ]
+        public PropertyComboBox Drive { get; set; } = new PropertyComboBox();
+
+
+        protected override void DoConstruct()
+        {
+            base.DoConstruct();
+            PopulateOpticalDrives();
+            CheckCurrentItem();
+        }
+
+
+        public override string Brief()
+        {
+            return Name + " " + Drive.CurrentItem ;
+        }
+
+        public override bool IsValid()
+        {   
+            //This object is valid if our current item is contained in our drive list
+            return Drive.Items.Contains(Drive.CurrentItem);
+        }
+
+        protected override void DoExecute()
+        {
+            DriveEject(Drive.CurrentItem);
+        }
+
+
+        private void CheckCurrentItem()
+        {
+            if (!Drive.Items.Contains(Drive.CurrentItem) && Drive.Items.Count>0)
+            {
+                //Current item unknown, reset it then
+                Drive.CurrentItem = Drive.Items[0];
+            }
+        }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        private void PopulateOpticalDrives()
+        {
+            //Reset our list of drives
+            Drive.Items = new List<string>();
+            //Go through each drives on our system and collected the optical ones in our list
+            DriveInfo[] allDrives = DriveInfo.GetDrives();
+            foreach (DriveInfo d in allDrives)
+            {
+                Debug.WriteLine("Drive " + d.Name);
+                Debug.WriteLine("  Drive type: {0}", d.DriveType);
+
+                if (d.DriveType == DriveType.CDRom)
+                {
+                    //This is an optical drive, add it now
+                    Drive.Items.Add(d.Name.Substring(0, 2));
+                }
+            }
+        }
+
+
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <param name="aPrefix"></param>
+        static private void CheckLastError(string aPrefix)
+        {
+            string errorMessage = new Win32Exception(Marshal.GetLastWin32Error()).Message;
+            Debug.WriteLine(aPrefix + Marshal.GetLastWin32Error().ToString() + ": " + errorMessage);
+        }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <param name="data"></param>
+        /// <returns></returns>
+        static private IntPtr MarshalToPointer(object data)
+        {
+            IntPtr buf = Marshal.AllocHGlobal(
+                Marshal.SizeOf(data));
+            Marshal.StructureToPtr(data,
+                buf, false);
+            return buf;
+        }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <returns></returns>
+        static private SafeFileHandle OpenVolume(string aDriveName)
+        {
+            return Function.CreateFile("\\\\.\\" + aDriveName,
+                               SharpLib.Win32.FileAccess.GENERIC_READ,
+                               SharpLib.Win32.FileShare.FILE_SHARE_READ | SharpLib.Win32.FileShare.FILE_SHARE_WRITE,
+                               IntPtr.Zero,
+                               CreationDisposition.OPEN_EXISTING,
+                               0,
+                               IntPtr.Zero);
+        }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <param name="aVolume"></param>
+        /// <returns></returns>
+        static private bool LockVolume(SafeFileHandle aVolume)
+        {
+            //Hope that's doing what I think it does
+            IntPtr dwBytesReturned = new IntPtr();
+            //Should not be needed but I'm not sure how to pass NULL in there.
+            OVERLAPPED overlapped = new OVERLAPPED();
+
+            int tries = 0;
+            const int KMaxTries = 100;
+            const int KSleepTime = 10;
+            bool success = false;
+
+            while (!success && tries < KMaxTries)
+            {
+                success = Function.DeviceIoControl(aVolume, Const.FSCTL_LOCK_VOLUME, IntPtr.Zero, 0, IntPtr.Zero, 0, dwBytesReturned, ref overlapped);
+                System.Threading.Thread.Sleep(KSleepTime);
+                tries++;
+            }
+
+            CheckLastError("Lock volume: ");
+
+            return success;
+        }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <param name="aVolume"></param>
+        /// <returns></returns>
+        static private bool DismountVolume(SafeFileHandle aVolume)
+        {
+            //Hope that's doing what I think it does
+            IntPtr dwBytesReturned = new IntPtr();
+            //Should not be needed but I'm not sure how to pass NULL in there.
+            OVERLAPPED overlapped = new OVERLAPPED();
+
+            bool res = Function.DeviceIoControl(aVolume, Const.FSCTL_DISMOUNT_VOLUME, IntPtr.Zero, 0, IntPtr.Zero, 0, dwBytesReturned, ref overlapped);
+            CheckLastError("Dismount volume: ");
+            return res;
+        }
+
+
+
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <param name="aVolume"></param>
+        /// <param name="aPreventRemoval"></param>
+        /// <returns></returns>
+        static private bool PreventRemovalOfVolume(SafeFileHandle aVolume, bool aPreventRemoval)
+        {
+            //Hope that's doing what I think it does
+            IntPtr dwBytesReturned = new IntPtr();
+            //Should not be needed but I'm not sure how to pass NULL in there.
+            OVERLAPPED overlapped = new OVERLAPPED();
+            //
+            PREVENT_MEDIA_REMOVAL preventMediaRemoval = new PREVENT_MEDIA_REMOVAL();
+            preventMediaRemoval.PreventMediaRemoval = Convert.ToByte(aPreventRemoval);
+            IntPtr preventMediaRemovalParam = MarshalToPointer(preventMediaRemoval);
+
+            bool result = Function.DeviceIoControl(aVolume, Const.IOCTL_STORAGE_MEDIA_REMOVAL, preventMediaRemovalParam, Convert.ToUInt32(Marshal.SizeOf(preventMediaRemoval)), IntPtr.Zero, 0, dwBytesReturned, ref overlapped);
+            CheckLastError("Media removal: ");
+            Marshal.FreeHGlobal(preventMediaRemovalParam);
+
+            return result;
+        }
+
+        /// <summary>
+        /// Eject optical drive media opening the tray if any.
+        /// </summary>
+        /// <param name="aVolume"></param>
+        /// <returns></returns>
+        static private bool MediaEject(SafeFileHandle aVolume)
+        {
+            //Hope that's doing what I think it does
+            IntPtr dwBytesReturned = new IntPtr();
+            //Should not be needed but I'm not sure how to pass NULL in there.
+            OVERLAPPED overlapped = new OVERLAPPED();
+
+            bool res = Function.DeviceIoControl(aVolume, Const.IOCTL_STORAGE_EJECT_MEDIA, IntPtr.Zero, 0, IntPtr.Zero, 0, dwBytesReturned, ref overlapped);
+            CheckLastError("Media eject: ");
+            return res;
+        }
+
+        /// <summary>
+        /// Close an optical drive tray.
+        /// </summary>
+        /// <param name="aVolume"></param>
+        /// <returns></returns>
+        static private bool MediaLoad(SafeFileHandle aVolume)
+        {
+            //Hope that's doing what I think it does
+            IntPtr dwBytesReturned = new IntPtr();
+            //Should not be needed but I'm not sure how to pass NULL in there.
+            OVERLAPPED overlapped = new OVERLAPPED();
+
+            bool res = Function.DeviceIoControl(aVolume, Const.IOCTL_STORAGE_LOAD_MEDIA, IntPtr.Zero, 0, IntPtr.Zero, 0, dwBytesReturned, ref overlapped);
+            CheckLastError("Media load: ");
+            return res;
+        }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <param name="aVolume"></param>
+        /// <returns></returns>
+        static private bool StorageCheckVerify(SafeFileHandle aVolume)
+        {
+            //Hope that's doing what I think it does
+            IntPtr dwBytesReturned = new IntPtr();
+            //Should not be needed but I'm not sure how to pass NULL in there.
+            OVERLAPPED overlapped = new OVERLAPPED();
+
+            bool res = Function.DeviceIoControl(aVolume, Const.IOCTL_STORAGE_CHECK_VERIFY2, IntPtr.Zero, 0, IntPtr.Zero, 0, dwBytesReturned, ref overlapped);
+
+            CheckLastError("Check verify: ");
+
+            return res;
+        }
+
+
+        /// <summary>
+        /// Perform media ejection.
+        /// </summary>
+        static private void DriveEject(string aDrive)
+        {
+            string drive = aDrive;
+            if (drive.Length != 2)
+            {
+                //Not a proper drive spec.
+                //Probably 'None' selected.
+                return;
+            }
+
+            SafeFileHandle handle = OpenVolume(drive);
+            if (handle.IsInvalid)
+            {
+                CheckLastError("ERROR: Failed to open volume: ");
+                return;
+            }
+
+            if (LockVolume(handle) && DismountVolume(handle))
+            {
+                Debug.WriteLine("Volume was dismounted.");
+
+                if (PreventRemovalOfVolume(handle, false))
+                {
+                    //StorageCheckVerify(handle);
+
+                    DateTime before;
+                    before = DateTime.Now;
+                    bool ejectSuccess = MediaEject(handle);
+                    double ms = (DateTime.Now - before).TotalMilliseconds;
+
+                    //We assume that if it take more than a certain time to for eject to execute it means we actually ejected.
+                    //If our eject completes too rapidly we assume the tray is already open and we will try to close it. 
+                    if (ejectSuccess && ms > 100)
+                    {
+                        Debug.WriteLine("Media was ejected");
+                    }
+                    else if (MediaLoad(handle))
+                    {
+                        Debug.WriteLine("Media was loaded");
+                    }
+                }
+            }
+            else
+            {
+                Debug.WriteLine("Volume lock or dismount failed.");
+            }
+
+            //This is needed to make sure we can open the volume next time around
+            handle.Dispose();
+        }
+
     }
 }
diff -r 0a956121c273 -r cc2251d065db SharpLibEar/Event.cs
--- a/SharpLibEar/Event.cs	Thu Aug 18 20:14:30 2016 +0200
+++ b/SharpLibEar/Event.cs	Fri Aug 19 17:12:54 2016 +0200
@@ -18,16 +18,22 @@
                 Description = "When enabled an event instance can be triggered."
             )
         ]
-        public bool Enabled { get; set; }
+        public bool Enabled { get; set; } = true;
 
         [DataMember]
         public List<Action> Actions = new List<Action>();
 
 
+        protected override void DoConstruct()
+        {
+            base.DoConstruct();
 
-        protected Event()
-        {
-            Enabled = true;
+            // TODO: Construct properties too
+            foreach (Action a in Actions)
+            {
+                a.Construct();
+            }
+
         }
 
 
diff -r 0a956121c273 -r cc2251d065db SharpLibEar/Manager.cs
--- a/SharpLibEar/Manager.cs	Thu Aug 18 20:14:30 2016 +0200
+++ b/SharpLibEar/Manager.cs	Fri Aug 19 17:12:54 2016 +0200
@@ -15,8 +15,7 @@
     /// Users can implement their own events and actions.
     /// </summary>
     [DataContract]
-    [KnownType("DerivedTypes")]
-    public class Manager
+    public class Manager: Object
     {
         /// <summary>
         /// Our events instances.
@@ -24,23 +23,24 @@
         [DataMember]
         public List<Event> Events;
 
-        /// <summary>
-        /// Constructor
-        /// </summary>
-        public Manager()
-        {
-            Init();
-        }
 
         /// <summary>
         /// Executes after internalization took place.
         /// </summary>
-        public void Init()
+        protected override void DoConstruct()
         {
+            base.DoConstruct();
+
             if (Events == null)
             {
                 Events = new List<Event>();
             }
+
+            // TODO: Object properties should be constructed too
+            foreach (Event e in Events)
+            {
+                e.Construct();
+            }
             
         }
 
@@ -86,15 +86,5 @@
                 }
             }
         }
-
-        /// <summary>
-        /// Allow extending our data contract.
-        /// See KnownType above.
-        /// </summary>
-        /// <returns></returns>
-        private static IEnumerable<Type> DerivedTypes()
-        {
-            return SharpLib.Utils.Reflection.GetDerivedTypes<Manager>();
-        }
     }
 }
\ No newline at end of file
diff -r 0a956121c273 -r cc2251d065db SharpLibEar/Object.cs
--- a/SharpLibEar/Object.cs	Thu Aug 18 20:14:30 2016 +0200
+++ b/SharpLibEar/Object.cs	Fri Aug 19 17:12:54 2016 +0200
@@ -21,6 +21,33 @@
     [KnownType("DerivedTypes")]
     public abstract class Object: IComparable
     {
+        private bool iConstructed = false;
+
+        public Object()
+        {
+            Construct();
+        }
+
+        /// <summary>
+        /// Needed as our constructor is not called following internalization.
+        /// </summary>
+        public void Construct()
+        {
+            if (!iConstructed)
+            {
+                DoConstruct();
+                iConstructed = true;
+            }
+        }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        protected virtual void DoConstruct()
+        {
+
+        }
+
         /// <summary>
         /// Static object name.
         /// </summary>
diff -r 0a956121c273 -r cc2251d065db SharpLibEar/PropertyComboBox.cs
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/SharpLibEar/PropertyComboBox.cs	Fri Aug 19 17:12:54 2016 +0200
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.Serialization;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SharpLib.Ear
+{
+    /// <summary>
+    /// ComboBox property
+    /// </summary>
+    [DataContract]
+    [AttributeObject(Id = "Property.ComboBox", Name = "ComboBox", Description = "ComboBox property.")]
+    public class PropertyComboBox : Object
+    {
+        public IList<string> Items { get; set; } = new List<string>();
+
+        [DataMember]
+        public string CurrentItem { get; set; }
+    }
+}
diff -r 0a956121c273 -r cc2251d065db SharpLibEar/SharpLibEar.csproj
--- a/SharpLibEar/SharpLibEar.csproj	Thu Aug 18 20:14:30 2016 +0200
+++ b/SharpLibEar/SharpLibEar.csproj	Fri Aug 19 17:12:54 2016 +0200
@@ -48,6 +48,10 @@
     <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
   </PropertyGroup>
   <ItemGroup>
+    <Reference Include="SharpLibWin32, Version=1.0.0.0, Culture=neutral, processorArchitecture=x86">
+      <HintPath>..\packages\SharpLibWin32.0.0.9\lib\net20\SharpLibWin32.dll</HintPath>
+      <Private>True</Private>
+    </Reference>
     <Reference Include="System" />
     <Reference Include="System.Core" />
     <Reference Include="System.Runtime.Serialization" />
@@ -73,6 +77,7 @@
     <Compile Include="Manager.cs" />
     <Compile Include="Object.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="PropertyComboBox.cs" />
     <Compile Include="PropertyFile.cs" />
   </ItemGroup>
   <ItemGroup>
@@ -81,6 +86,9 @@
       <Name>SharpLibUtils</Name>
     </ProjectReference>
   </ItemGroup>
+  <ItemGroup>
+    <None Include="packages.config" />
+  </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
        Other similar extension points exist, see Microsoft.Common.targets.
diff -r 0a956121c273 -r cc2251d065db SharpLibEar/packages.config
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/SharpLibEar/packages.config	Fri Aug 19 17:12:54 2016 +0200
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="SharpLibWin32" version="0.0.9" targetFramework="net46" />
+</packages>
\ No newline at end of file