using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Linq; using System.Reflection; using System.Security.Permissions; using System.Windows.Forms; using DelftTools.Controls; using DelftTools.Shell.Core; using DelftTools.Shell.Gui; using DelftTools.Shell.Gui.Forms; using DelftTools.Utils.Aop; using DelftTools.Utils.Collections; using DelftTools.Utils.PropertyBag.Dynamic; using DeltaShell.Gui.Properties; using log4net; using PropertyInfo = DelftTools.Shell.Gui.PropertyInfo; namespace DeltaShell.Gui.Forms.PropertyGrid { /// TODO: get rid of the properties class or create them runtime like this: /// http://stackoverflow.com/questions/313822/how-to-modify-propertygrid-at-runtime-add-remove-property-and-dynamic-types-enum /// Having to create property classes for everty state of the object is too much code /// /// PropertyGrid extends the functionality of the PropertyGrid by extra support /// for objects of different types. The default behaviour of the PropertyGrid is to show /// only the common properties of the objects in the selectedObjects array. In some cases /// it is desirable to add an extra filter for objects types. /// An example were this behaviour is requested is the 1d schematisation editor. The user /// selects the objects in the GIS oriented view by tracking a rectangle. The objects /// inside the rectangle are set as selectedObjects in a propertygrid. In many cases the /// objects in the selection rectangle will be of different types; this results in a very /// limited subset of shared properties. /// Typically a user is only interested in river profiles or culverts. This /// PropertyGrid user control automatically makes a subdivision of the different /// types in the selectedObjects array and offers the user the chance to make a selection /// via a combobox. /// /// Note the combobox at the Top of the propertygrid tries to mimic the behaviour of the /// combobox in Visual Studio. If 1 object is selected the Id is shown bold followed by /// the type description. /// /// public sealed partial class PropertyGrid : UserControl, IPropertyGrid, IObserver { private static readonly ILog Log = LogManager.GetLogger(typeof(PropertyGrid)); /// /// todo: This is still an unwanted dependency. PropertyGrid uses gui to subscribe to the SelectionChanged /// delegate and in responce queries the gui.Selection /// nicer? : custom public delegate in IPropertyGrid with selection as parameter /// private readonly IGui gui; /// /// objectTypes is a collection of used types. The type of the object is used as key, /// the value is the number of occurences in selectedObjects /// private readonly Dictionary objectTypes = new Dictionary(); // The selected object as they are available to the outside world private object[] selectedObjects; private INotifyPropertyChanged notifiableProperty; public PropertyGrid(IGui gui) { InitializeComponent(); MinimumSize = new Size(200, 200); this.gui = gui; gui.SelectionChanged += GuiSelectionChanged; // removing "property tabs" button and separator before it var strip = propertyGrid1.Controls.OfType().ToList()[0] as ToolStrip; strip.Items[3].Visible = false; strip.Items[4].Visible = false; // TODO: make timer start only when property was changed and then stop refreshTimer = new Timer(); refreshTimer.Tick += delegate { if (IsInEditMode(propertyGrid1)) { return; } propertyGrid1.Refresh(); }; refreshTimer.Interval = 300; refreshTimer.Enabled = true; refreshTimer.Start(); } //from: http://stackoverflow.com/questions/7553423/c-sharp-propertygrid-check-if-a-value-is-currently-beeing-edited private static bool IsInEditMode(System.Windows.Forms.PropertyGrid grid) { if (grid == null) { throw new ArgumentNullException("grid"); } var gridView = (Control) grid.GetType().GetField("gridView", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(grid); var edit = (Control) gridView.GetType().GetField("edit", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(gridView); var dropDownHolder = (Control) gridView.GetType().GetField("dropDownHolder", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(gridView); return ((edit != null) && (edit.Visible & edit.Focused)) || ((dropDownHolder != null) && (dropDownHolder.Visible)); } public void UpdateObserver() { // refresh expected here } public object GetObjectProperties(object sourceData) { if (sourceData == null) { return null; } // Obtain all property information var propertyInfos = gui.Plugins.SelectMany(p => p.GetPropertyInfos()).ToList(); // 1. Match property information based on ObjectType and on AdditionalDataCheck propertyInfos = propertyInfos.Where(pi => pi.ObjectType.IsAssignableFrom(sourceData.GetType()) && (pi.AdditionalDataCheck == null || pi.AdditionalDataCheck(sourceData))).ToList(); // 2. Match property information based on object type inheritance propertyInfos = FilterPropertyInfoByTypeInheritance(propertyInfos, pi => pi.ObjectType); // 3. Match property information based on property type inheritance propertyInfos = FilterPropertyInfoByTypeInheritance(propertyInfos, pi => pi.PropertyType); if (propertyInfos.Count == 0) { // No (or multiple) object properties found: return 'null' so that no object properties are shown in the property grid return null; } if (propertyInfos.Count > 1) { // 4. We assume that the propertyInfos with AdditionalDataCheck are the most specific propertyInfos = propertyInfos.Where(pi => pi.AdditionalDataCheck != null).ToList(); } if (propertyInfos.Count == 1) { return CreateObjectProperties(propertyInfos.ElementAt(0), sourceData); } Log.Debug(Resources.PropertyGrid_GetObjectProperties_Multiple_object_property_instances_found_for_the_same_data_object__no_object_properties_are_displayed_in_the_property_grid); return null; } private object[] SelectedObjects { get { return selectedObjects; } [InvokeRequired] set { selectedObjects = value; OnSelectedObjectsChanged(); } } private object SelectedObject { get { if (selectedObjects == null) { return null; } return selectedObjects[0]; } set { // If the object is only one list, show all the contained items IEnumerable ienum = value as IEnumerable; if (ienum != null) { ArrayList objects = new ArrayList(); IEnumerator obj = ienum.GetEnumerator(); // Create array of the items in the list while (obj.MoveNext()) { objects.Add(obj.Current); } SelectedObjects = objects.Count > 0 ? objects.ToArray() : null; // If this was an empty list, set the property grid to null } else { // Show a single object object[] objects = new object[1]; objects[0] = value; SelectedObjects = objects; } } } private void GuiSelectionChanged(object sender, EventArgs e) { if (IsDisposed) { return; //event may fire when propertygrid is already disposed. } if (observableProperty != null) { observableProperty.Detach(this); } notifiableProperty = null; notifiableCollection = null; var selection = gui.Selection; if (selection == null) { SelectedObject = null; return; } notifiableProperty = selection as INotifyPropertyChanged; observableProperty = selection as IObservable; if (observableProperty != null) { observableProperty.Attach(this); } notifiableCollection = selection as INotifyCollectionChanged; object propertyObject; if (selection is IEnumerable) { // Try to get a object properties for the enumerable directly propertyObject = GetObjectProperties(selection); if (propertyObject == null || propertyObject == selection) { // Otherwise, create an array list with object properties for the individual elements of the enumerable var enumerable = (IEnumerable) selection; var providers = new ArrayList(); foreach (var element in enumerable) { var o = GetObjectProperties(element); if (o != null) { providers.Add(o); } } propertyObject = providers; } } else { propertyObject = GetObjectProperties(selection); } if (selectedObjects != propertyObject) { SelectedObject = propertyObject; } } private List FilterPropertyInfoByTypeInheritance(List propertyInfo, Func getTypeAction) { var propertyInfoCount = propertyInfo.Count(); var propertyInfoWithUnInheritedType = propertyInfo.ToList(); for (var i = 0; i < propertyInfoCount; i++) { var firstType = getTypeAction(propertyInfo.ElementAt(i)); for (var j = 0; j < propertyInfoCount; j++) { if (i == j) { continue; } var secondType = getTypeAction(propertyInfo.ElementAt(j)); if (firstType != secondType && firstType.IsAssignableFrom(secondType)) { propertyInfoWithUnInheritedType.Remove(propertyInfo.ElementAt(i)); break; } } } return propertyInfoWithUnInheritedType.Any() ? propertyInfoWithUnInheritedType.ToList() // One or more specific property information objects found: return the filtered list : propertyInfo; // No specific property information found: return the original list } private object CreateObjectProperties(PropertyInfo propertyInfo, object sourceData) { try { // Try to create object properties for the source data var objectProperties = propertyInfo.CreateObjectProperties(sourceData); // Return a dynamic property bag containing the created object properties return objectProperties is DynamicPropertyBag ? (object) objectProperties : new DynamicPropertyBag(objectProperties); } catch (Exception) { Log.Debug(Resources.PropertyGrid_CreateObjectProperties_Could_not_create_object_properties_for_the_data); // Directly return the source data (TODO: Shouldn't we return "null" instead?) return sourceData; } } /// /// OnSelectedObjectsChanged /// If the selected objects changed rebuild the internal dictionary that counts the number of /// objects of each type. /// private void OnSelectedObjectsChanged() { objectTypes.Clear(); if (SelectedObjects == null) { propertyGrid1.SelectedObject = null; return; } if (SelectedObjects[0] == null) { if (propertyGrid1.SelectedObjects.Length > 0) { propertyGrid1.SelectedObjects = new object[0]; } return; } if (SelectedObjects.Length == 1) { var selectedType = GetRelevantType(SelectedObjects[0]); Log.DebugFormat(Resources.PropertyGrid_OnSelectedObjectsChanged_Selected_object_of_type___0_, selectedType.Name); objectTypes.Add(selectedType, 1); } else { Log.DebugFormat(Resources.PropertyGrid_OnSelectedObjectsChanged_Selected_multiple_objects_); int count = 0; for (int i = 0; i < SelectedObjects.Length; i++) { var selectedType = GetRelevantType(SelectedObjects[i]); if (!objectTypes.ContainsKey(selectedType)) { objectTypes.Add(selectedType, 1); } else { objectTypes[selectedType] = objectTypes[selectedType] + 1; } //Aggregate logging results for speed reasons: string currOne = selectedType.Name; int hash = currOne.GetHashCode(); bool printAggregate = false; count++; if (i < (selectedObjects.Length - 1)) //not at the end yet, look at next one { string nextOne = GetRelevantType(selectedObjects[i + 1]).Name; int nextHash = nextOne.GetHashCode(); printAggregate = hash != nextHash; } else //last one, print ourselves, plus previous if same { //(we know there are at least two objects in the list) string prevOne = GetRelevantType(selectedObjects[i - 1]).Name; int prevHash = prevOne.GetHashCode(); if (hash != prevHash) { Log.DebugFormat(" 1x: {0}", currOne); } else { printAggregate = true; } } if (printAggregate) { Log.DebugFormat(" {0}x: {1}", count, currOne); count = 0; } } } propertyGrid1.SelectedObject = SelectedObjects.Count() > 1 ? SelectedObjects[1] : SelectedObjects[0]; } private static Type GetRelevantType(object obj) { if (obj is DynamicPropertyBag) { var bag = obj as DynamicPropertyBag; return bag.GetContentType(); } return obj.GetType(); } private void PropertyGrid1PropertySortChanged(object sender, EventArgs e) { // Needed for maintaining property order if (propertyGrid1.PropertySort == PropertySort.CategorizedAlphabetical) { propertyGrid1.PropertySort = PropertySort.Categorized; } } #region IPropertyGrid Members public object Data { get { return SelectedObject; } set { SelectedObject = value; } } public Image Image { get { return Resources.PropertiesHS; } set {} } public void EnsureVisible(object item) {} public ViewInfo ViewInfo { get; set; } #endregion #region enable tab key navigation on propertygrid private readonly Timer refreshTimer; private INotifyCollectionChanged notifiableCollection; private IObservable observableProperty; /// /// Gets or sets whether to expand an item when pressing tab. /// /// /// When true items are also unexpanded when pressing shift-tab. /// Note that the enter key will always work to expand. /// public bool ExpandOnTab { get; set; } /// /// Do special processing for Tab key. /// http://www.codeproject.com/csharp/wdzPropertyGridUtils.asp /// /// /// /// [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] protected override bool ProcessCmdKey(ref Message msg, Keys keyData) { if ((keyData == Keys.Tab) || (keyData == (Keys.Tab | Keys.Shift))) { GridItem selectedItem = propertyGrid1.SelectedGridItem; GridItem root = selectedItem; if (selectedItem == null) { return false; } while (root.Parent != null) { root = root.Parent; } // Find all expanded items and put them in a list. ArrayList items = new ArrayList(); AddExpandedItems(root, items); // Find selectedItem. int foundIndex = items.IndexOf(selectedItem); if ((keyData & Keys.Shift) == Keys.Shift) { foundIndex--; if (foundIndex < 0) { foundIndex = items.Count - 1; } propertyGrid1.SelectedGridItem = (GridItem) items[foundIndex]; if (ExpandOnTab && (propertyGrid1.SelectedGridItem.GridItems.Count > 0)) { propertyGrid1.SelectedGridItem.Expanded = false; } } else { foundIndex++; if (items.Count > 0) { if (foundIndex >= items.Count) { foundIndex = 0; } propertyGrid1.SelectedGridItem = (GridItem) items[foundIndex]; } if (ExpandOnTab && (propertyGrid1.SelectedGridItem.GridItems.Count > 0)) { propertyGrid1.SelectedGridItem.Expanded = true; } } return true; } return base.ProcessCmdKey(ref msg, keyData); } private static void AddExpandedItems(GridItem parent, IList items) { if (parent.PropertyDescriptor != null) { items.Add(parent); } if (parent.Expanded) { foreach (GridItem child in parent.GridItems) { AddExpandedItems(child, items); } } } #endregion } }