/// <summary> /// Get properties values from our generated input fields /// </summary> private void FetchPropertiesValue(T aObject) { int ctrlIndex = 0; //For each of our properties foreach (PropertyInfo pi in aObject.GetType().GetProperties()) { //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 SetObjectPropertyValueFromControl(aObject, pi, iTableLayoutPanel.Controls[ctrlIndex + 1]); //+1 otherwise we get the label ctrlIndex += 2; //Jump over the label too } }
/// <summary> /// Update our table layout. /// Will instantiated every field control as defined by our object. /// </summary> /// <param name="aLayout"></param> private void UpdateControls() { iTableLayoutPanel.SuspendLayout(); // Avoid flicker toolTip.RemoveAll(); //Debug.Print("UpdateTableLayoutPanel") //First clean our current panel iTableLayoutPanel.Controls.Clear(); iTableLayoutPanel.RowStyles.Clear(); iTableLayoutPanel.ColumnStyles.Clear(); iTableLayoutPanel.RowCount = 0; //We always want two columns: one for label and one for the field iTableLayoutPanel.ColumnCount = 2; iTableLayoutPanel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); iTableLayoutPanel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); if (Object == null) { iTableLayoutPanel.ResumeLayout(true); //Just drop it return; } // Tell our object to prepare for edit Object.CurrentState = SharpLib.Ear.Object.State.PrepareEdit; Object.PropertyChanged -= PropertyChangedEventHandlerThreadSafe; // Most important to avoid accumulating handlers // UpdateStaticControls(); //IEnumerable<PropertyInfo> properties = aObject.GetType().GetProperties().Where( // prop => Attribute.IsDefined(prop, typeof(AttributeObjectProperty))); //TODO: Do this whenever a field changes iLabelBrief.Text = Object.Brief(); foreach (PropertyInfo pi in Object.GetType().GetProperties()) { AttributeObjectProperty[] attributes = ((AttributeObjectProperty[])pi.GetCustomAttributes(typeof(AttributeObjectProperty), true)); if (attributes.Length != 1) { continue; } AttributeObjectProperty attribute = attributes[0]; //Before anything we need to check if that kind of property is supported by our UI //Create the editor Control ctrl = CreateControlForProperty(pi, attribute, Object); if (ctrl == null) { //Property type not supported continue; } // Associate our object property with that control then // That will enable us to update just that control when the property changes ctrl.Tag = pi; //Add a new row iTableLayoutPanel.RowCount++; iTableLayoutPanel.RowStyles.Add(new RowStyle(SizeType.AutoSize)); //Create the label Label label = new Label(); label.AutoSize = true; label.Dock = DockStyle.Fill; label.TextAlign = ContentAlignment.MiddleCenter; label.Text = attribute.Name; toolTip.SetToolTip(label, attribute.Description); iTableLayoutPanel.Controls.Add(label, 0, iTableLayoutPanel.RowCount - 1); //Add our editor to our form iTableLayoutPanel.Controls.Add(ctrl, 1, iTableLayoutPanel.RowCount - 1); //Add tooltip to editor too toolTip.SetToolTip(ctrl, attribute.Description); } //Enter object edit mode Object.CurrentState = SharpLib.Ear.Object.State.Edit; Object.PropertyChanged += PropertyChangedEventHandlerThreadSafe; iTableLayoutPanel.ResumeLayout(true); }
/// <summary> /// Create a control for the given property. /// TODO: Handle cases where a property value is null. That can be the case when extending an existing class and loading it from an older save. /// Though I guess that should really be taken care of in Ear.Object.Construct which is called once the object was internalized. /// </summary> /// <param name="aInfo"></param> /// <param name="aAttribute"></param> /// <param name="aObject"></param> /// <returns></returns> private Control CreateControlForProperty(PropertyInfo aInfo, AttributeObjectProperty aAttribute, T aObject) { if (aInfo.PropertyType == typeof(int) || aInfo.PropertyType == typeof(float)) { //Integer properties are using numeric editor NumericUpDown ctrl = new NumericUpDown(); ctrl.AutoSize = true; //ctrl.Dock = DockStyle.Fill; // Fill the whole table cell ctrl.Minimum = (decimal)aAttribute.Minimum; ctrl.Maximum = (decimal)aAttribute.Maximum; ctrl.Increment = (decimal)aAttribute.Increment; ctrl.DecimalPlaces = aAttribute.DecimalPlaces; ctrl.Value = decimal.Parse(aInfo.GetValue(aObject).ToString()); // Hook-in change notification after setting the value ctrl.ValueChanged += ControlValueChanged; return(ctrl); } else if (aInfo.PropertyType.IsEnum) { //Enum properties are using combo box ComboBox ctrl = new ComboBox(); 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); 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 measure text ourselves 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); } cbSize.Width += 10; // Account for margin //Make sure our combobox is large enough ctrl.MinimumSize = cbSize; // Instantiate our enum object enumValue = Activator.CreateInstance(aInfo.PropertyType); enumValue = aInfo.GetValue(aObject); //Set the current item ctrl.SelectedItem = enumValue.ToString(); // Hook-in change notification after setting the value ctrl.SelectedIndexChanged += ControlValueChanged; return(ctrl); } else if (aInfo.PropertyType == typeof(bool)) { CheckBox ctrl = new CheckBox(); ctrl.AutoSize = true; ctrl.Text = aAttribute.Description; ctrl.Checked = (bool)aInfo.GetValue(aObject); // Hook-in change notification after setting the value ctrl.CheckedChanged += ControlValueChanged; return(ctrl); } else if (aInfo.PropertyType == typeof(string)) { TextBox ctrl = new TextBox(); ctrl.AutoSize = true; ctrl.Dock = DockStyle.Fill; // Fill the whole table cell ctrl.Text = (string)aInfo.GetValue(aObject); // Multiline setup ctrl.Multiline = aAttribute.Multiline; if (ctrl.Multiline) { ctrl.AcceptsReturn = true; ctrl.ScrollBars = ScrollBars.Vertical; // Adjust our height as needed Size size = ctrl.Size; size.Height += ctrl.Font.Height * (aAttribute.HeightInLines - 1); ctrl.MinimumSize = size; } // Hook-in change notification after setting the value ctrl.TextChanged += ControlValueChanged; return(ctrl); } else if (aInfo.PropertyType == typeof(PropertyFile)) { // We have a file property // Create a button that will trigger the open file dialog to select our file. Button ctrl = new Button(); ctrl.AutoSize = true; ctrl.Text = ((PropertyFile)aInfo.GetValue(aObject)).FullPath; // Add lambda expression to Click event ctrl.Click += (sender, e) => { // Create open file dialog OpenFileDialog ofd = new OpenFileDialog(); ofd.RestoreDirectory = true; // Use file filter specified by our property ofd.Filter = aAttribute.Filter; // Show our dialog if (SharpLib.Forms.DlgBox.ShowDialog(ofd) == DialogResult.OK) { // Fetch selected file name ctrl.Text = ofd.FileName; } }; // Hook-in change notification after setting the value ctrl.TextChanged += ControlValueChanged; return(ctrl); } else if (aInfo.PropertyType == typeof(PropertyComboBox)) { //ComboBox property PropertyComboBox property = ((PropertyComboBox)aInfo.GetValue(aObject)); ComboBox ctrl = new ComboBox(); ctrl.AutoSize = true; //ctrl.Sorted = property.Sorted; // Sorted is not supported and does not work using DataSource, can result in the wrong item being selected ctrl.DropDownStyle = ComboBoxStyle.DropDownList; // Use DataSource with optional DisplayMember and ValueMember, that also works for plain string collection ctrl.DisplayMember = property.DisplayMember; ctrl.ValueMember = property.ValueMember; //ctrl.DataSource = ((PropertyComboBox)aInfo.GetValue(aObject)).Items; UpdateControlFromProperty(ctrl, aInfo, aObject); return(ctrl); } else if (aInfo.PropertyType == typeof(PropertyButton)) { // We have a button property // Create a button that will trigger the custom action. Button ctrl = new Button(); ctrl.AutoSize = true; ctrl.Text = ((PropertyButton)aInfo.GetValue(aObject)).Text; // Hook in click event ctrl.Click += ((PropertyButton)aInfo.GetValue(aObject)).ClickEventHandler; // Hook-in change notification after setting the value ctrl.TextChanged += ControlValueChanged; return(ctrl); } else if (aInfo.PropertyType == typeof(PropertyCheckedListBox)) { //CheckedListBox property PropertyCheckedListBox property = ((PropertyCheckedListBox)aInfo.GetValue(aObject)); CheckedListBox ctrl = new CheckedListBox(); ctrl.AutoSize = true; ctrl.Sorted = property.Sorted; ctrl.CheckOnClick = true; // Populate our box with list items foreach (string item in property.Items) { int index = ctrl.Items.Add(item); // Check items if needed if (property.CheckedItems.Contains(item)) { ctrl.SetItemChecked(index, true); } } // // Hook-in change notification after setting the value // That's essentially making sure title/brief gets updated as items are checked or unchecked // This looks convoluted as we are working around the fact that ItemCheck event is triggered before the model is updated. // See: https://stackoverflow.com/a/48645552/3969362 ctrl.ItemCheck += (s, e) => BeginInvoke((MethodInvoker)(() => ControlValueChanged(s, e))); return(ctrl); } //TODO: add support for other control type here return(null); }