|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Drawing;
using System.Drawing.Design;
namespace System.Windows.Forms.PropertyGridInternal;
internal partial class PropertyDescriptorGridEntry : GridEntry
{
private TypeConverter? _exceptionConverter;
private UITypeEditor? _exceptionEditor;
private bool _isSerializeContentsProperty;
private bool? _parensAroundName;
private IPropertyValueUIService? _propertyValueUIService;
protected IEventBindingService? _eventBindings;
private bool _propertyValueUIServiceChecked;
private PropertyValueUIItem[]? _propertyValueUIItems;
private Rectangle[]? _uiItemRects;
private bool _readOnlyVerified;
private bool _forceRenderReadOnly;
private string? _helpKeyword;
private string? _toolTipText;
private readonly bool _hide;
private const int LogicalImageSize = 8;
private static int s_imageSize = LogicalImageSize;
private static bool s_isScalingInitialized;
private static IEventBindingService? s_targetBindingService;
private static IComponent? s_targetComponent;
private static EventDescriptor? s_targetEventdesc;
internal PropertyDescriptorGridEntry(
PropertyGrid ownerGrid,
GridEntry parent,
PropertyDescriptor propertyDescriptor,
bool hide)
: base(ownerGrid, parent)
{
_hide = hide;
PropertyDescriptor = propertyDescriptor;
Initialize(propertyDescriptor);
}
public override bool AllowMerge
=> PropertyDescriptor.GetAttribute<MergablePropertyAttribute>()?.IsDefaultAttribute() ?? true;
protected override AttributeCollection Attributes => PropertyDescriptor.Attributes;
public override string? HelpKeyword
{
get
{
if (_helpKeyword is not null)
{
return _helpKeyword;
}
object? owner = GetValueOwner();
if (owner is null)
{
return null;
}
if (PropertyDescriptor.TryGetAttribute(out HelpKeywordAttribute? helpAttribute) &&
!helpAttribute.IsDefaultAttribute())
{
return helpAttribute.HelpKeyword;
}
else if (this is ImmutablePropertyDescriptorGridEntry)
{
_helpKeyword = PropertyName;
GridEntry entry = this;
while (entry.ParentGridEntry is not null)
{
entry = entry.ParentGridEntry;
// For value classes, the equality will never work, so just try the type equality.
if (entry.PropertyValue == owner
|| (owner.GetType().IsValueType && owner.GetType() == entry.PropertyValue?.GetType()))
{
_helpKeyword = $"{entry.PropertyName}.{_helpKeyword}";
break;
}
}
}
else
{
string? typeName;
Type? componentType = PropertyDescriptor.ComponentType;
if (componentType is not null && componentType.IsCOMObject)
{
typeName = TypeDescriptor.GetClassName(owner);
}
else
{
// Make sure this property is declared on a class that is related to the component we're
// looking at. If it's not, it could be a shadow property so we need to try to find the
// real property.
Type ownerType = owner.GetType();
if (componentType is not null && (!componentType.IsPublic || !componentType.IsAssignableFrom(ownerType)))
{
PropertyDescriptor? componentProperty = TypeDescriptor.GetProperties(ownerType)[PropertyName!];
componentType = componentProperty?.ComponentType;
}
typeName = componentType is null ? TypeDescriptor.GetClassName(owner) : componentType.FullName;
}
_helpKeyword = $"{typeName}.{PropertyDescriptor.Name}";
}
return _helpKeyword;
}
}
internal override string? LabelToolTipText => _toolTipText ?? base.LabelToolTipText;
internal override bool Enumerable => base.Enumerable && !IsPropertyReadOnly;
internal virtual bool IsPropertyReadOnly => PropertyDescriptor.IsReadOnly;
public override bool IsValueEditable => _exceptionConverter is null && !IsPropertyReadOnly && base.IsValueEditable;
public override bool NeedsDropDownButton => base.NeedsDropDownButton && !IsPropertyReadOnly;
internal bool ParensAroundName
{
get
{
if (!_parensAroundName.HasValue)
{
_parensAroundName = PropertyDescriptor.GetAttribute<ParenthesizePropertyNameAttribute>()?.NeedParenthesis ?? false;
}
return _parensAroundName.Value;
}
}
public override string PropertyCategory
{
get
{
string? category = PropertyDescriptor.Category;
if (category is null || category.Length == 0)
{
category = base.PropertyCategory;
}
return category;
}
}
public sealed override PropertyDescriptor PropertyDescriptor { get; }
public override string? PropertyDescription => PropertyDescriptor.Description;
public override string? PropertyLabel
{
get
{
string? label = PropertyDescriptor.DisplayName;
if (ParensAroundName)
{
label = $"({label})";
}
return label;
}
}
/// <summary>
/// Returns non-localized name of this property.
/// </summary>
public override string? PropertyName => PropertyDescriptor.Name;
public override Type? PropertyType => PropertyDescriptor.PropertyType;
/// <summary>
/// Gets or sets the value for the property that is represented by this GridEntry.
/// </summary>
public override object? PropertyValue
{
get
{
try
{
object? value = GetPropertyValue(GetValueOwner());
if (_exceptionConverter is not null)
{
// Undo the exception converter.
ClearFlags();
_exceptionConverter = null;
_exceptionEditor = null;
}
return value;
}
catch (Exception e)
{
if (_exceptionConverter is null)
{
// Clear the flags.
ClearFlags();
_exceptionConverter = new ExceptionConverter();
_exceptionEditor = new ExceptionEditor();
}
return e;
}
}
set
{
SetPropertyValue(GetValueOwner(), value, reset: false, undoText: null);
}
}
private IPropertyValueUIService? PropertyValueUIService
{
get
{
if (!_propertyValueUIServiceChecked && _propertyValueUIService is null)
{
_propertyValueUIService = (IPropertyValueUIService?)GetService(typeof(IPropertyValueUIService));
_propertyValueUIServiceChecked = true;
}
return _propertyValueUIService;
}
}
public override bool ShouldRenderReadOnly
{
get
{
if (base.ForceReadOnly || _forceRenderReadOnly)
{
return true;
}
// If read only editable is set, make sure it's valid.
if (PropertyDescriptor.IsReadOnly && !_readOnlyVerified && GetFlagSet(Flags.ReadOnlyEditable))
{
Type? propertyType = PropertyType;
if (propertyType is not null && (propertyType.IsArray || propertyType.IsValueType || propertyType.IsPrimitive))
{
SetFlag(Flags.ReadOnlyEditable, false);
SetFlag(Flags.RenderReadOnly, true);
_forceRenderReadOnly = true;
}
}
_readOnlyVerified = true;
return base.ShouldRenderReadOnly && !_isSerializeContentsProperty && !NeedsModalEditorButton;
}
}
internal override TypeConverter TypeConverter
{
get
{
if (_exceptionConverter is not null)
{
return _exceptionConverter;
}
_typeConverter ??= PropertyDescriptor.Converter;
return base.TypeConverter;
}
}
internal override UITypeEditor? UITypeEditor
{
get
{
if (_exceptionEditor is not null)
{
return _exceptionEditor;
}
Editor = (UITypeEditor?)PropertyDescriptor.GetEditor(typeof(UITypeEditor));
return base.UITypeEditor;
}
}
internal override void EditPropertyValue(PropertyGridView gridView)
{
base.EditPropertyValue(gridView);
if (!IsValueEditable)
{
if (PropertyDescriptor.TryGetAttribute(out RefreshPropertiesAttribute? refreshAttribute) &&
!refreshAttribute.RefreshProperties.Equals(RefreshProperties.None))
{
OwnerGridView?.Refresh(fullRefresh: refreshAttribute.Equals(RefreshPropertiesAttribute.All));
}
}
}
internal override Point GetLabelToolTipLocation(int mouseX, int mouseY)
{
if (_propertyValueUIItems is not null
&& _uiItemRects is not null
&& OwnerGridView is not null)
{
for (int i = 0; i < _propertyValueUIItems.Length; i++)
{
if (_uiItemRects[i].Contains(mouseX, OwnerGridView.GridEntryHeight / 2))
{
_toolTipText = _propertyValueUIItems[i].ToolTip;
return new Point(mouseX, mouseY);
}
}
}
_toolTipText = null;
return base.GetLabelToolTipLocation(mouseX, mouseY);
}
private object? GetPropertyValue(object? owner)
{
if (owner is ICustomTypeDescriptor descriptor)
{
owner = descriptor.GetPropertyOwner(PropertyDescriptor);
}
return PropertyDescriptor.GetValue(owner);
}
protected void Initialize(PropertyDescriptor propertyDescriptor)
{
_isSerializeContentsProperty = propertyDescriptor.SerializationVisibility == DesignerSerializationVisibility.Content;
if (!_hide && IsPropertyReadOnly)
{
SetFlag(Flags.TextEditable, false);
}
if (_isSerializeContentsProperty && TypeConverter.GetPropertiesSupported())
{
SetFlag(Flags.Expandable, true);
}
}
protected virtual void NotifyParentsOfChanges(GridEntry? entry)
{
// See if we need to notify the parent(s) up the chain.
while (entry is PropertyDescriptorGridEntry propertyEntry &&
propertyEntry.PropertyDescriptor.Attributes.Contains(NotifyParentPropertyAttribute.Yes))
{
// Find the next parent property with a different value owner.
object? owner = entry.GetValueOwner();
// When owner is an instance of a value type we can't use == in the following while condition.
bool isValueType = owner?.GetType().IsValueType ?? false;
// Find the next property descriptor with a different parent.
while (entry is not PropertyDescriptorGridEntry
|| isValueType ? owner!.Equals(entry.GetValueOwner()) : owner == entry.GetValueOwner())
{
entry = entry.ParentGridEntry;
if (entry is null)
{
break;
}
}
// Fire the change on the owner.
if (entry is not null)
{
owner = entry.GetValueOwner();
if (owner is not null)
{
ComponentChangeService?.OnComponentChanging(owner, entry.PropertyDescriptor);
ComponentChangeService?.OnComponentChanged(owner, entry.PropertyDescriptor);
}
// Clear the value so it paints correctly next time.
entry.ClearCachedValues(clearChildren: false);
OwnerGridView?.InvalidateGridEntryValue(entry);
}
}
}
protected override bool SendNotification(object? owner, Notify type)
{
if (owner is ICustomTypeDescriptor descriptor)
{
owner = descriptor.GetPropertyOwner(PropertyDescriptor);
}
switch (type)
{
case Notify.Reset:
SetPropertyValue(owner, value: null, reset: true, undoText: string.Format(SR.PropertyGridResetValue, PropertyName));
if (_propertyValueUIItems is not null)
{
for (int i = 0; i < _propertyValueUIItems.Length; i++)
{
_propertyValueUIItems[i].Reset();
}
}
_propertyValueUIItems = null;
return false;
case Notify.CanReset:
try
{
return PropertyDescriptor.CanResetValue(owner!)
|| (_propertyValueUIItems is not null && _propertyValueUIItems.Length > 0);
}
catch
{
if (_exceptionConverter is null)
{
ClearFlags();
_exceptionConverter = new ExceptionConverter();
_exceptionEditor = new ExceptionEditor();
}
return false;
}
case Notify.ShouldPersist:
try
{
return PropertyDescriptor.ShouldSerializeValue(owner!);
}
catch
{
if (_exceptionConverter is null)
{
ClearFlags();
_exceptionConverter = new ExceptionConverter();
_exceptionEditor = new ExceptionEditor();
}
return false;
}
case Notify.DoubleClick:
case Notify.Return:
_eventBindings ??= this.GetService<IEventBindingService>();
if (_eventBindings?.GetEvent(PropertyDescriptor) is not null)
{
return ViewEvent(owner, newHandler: null, eventDescriptor: null, alwaysNavigate: true);
}
break;
}
return false;
}
public override void OnComponentChanged()
{
base.OnComponentChanged();
// If we got this it means someone called ITypeDescriptorContext.OnComponentChanged.
// We need to echo that change up the inheritance in case the owner object isn't a sited component.
NotifyParentsOfChanges(this);
}
public override bool OnMouseClick(int x, int y, int count, MouseButtons button)
{
if (_propertyValueUIItems is not null
&& _uiItemRects is not null
&& count == 2
&& ((button & MouseButtons.Left) == MouseButtons.Left)
&& OwnerGridView is not null)
{
for (int i = 0; i < _propertyValueUIItems.Length; i++)
{
if (_uiItemRects[i].Contains(x, OwnerGridView.GridEntryHeight / 2))
{
_propertyValueUIItems[i].InvokeHandler(this, PropertyDescriptor, _propertyValueUIItems[i]);
return true;
}
}
}
return base.OnMouseClick(x, y, count, button);
}
public override void PaintLabel(
Graphics g,
Rectangle rect,
Rectangle clipRect,
bool selected,
bool paintFullLabel)
{
base.PaintLabel(g, rect, clipRect, selected, paintFullLabel);
IPropertyValueUIService? uiService = PropertyValueUIService;
if (uiService is null)
{
return;
}
_propertyValueUIItems = uiService.GetPropertyUIValueItems(this, PropertyDescriptor);
if (_propertyValueUIItems is null)
{
return;
}
if (_uiItemRects is null || _uiItemRects.Length != _propertyValueUIItems.Length)
{
_uiItemRects = new Rectangle[_propertyValueUIItems.Length];
}
if (!s_isScalingInitialized)
{
s_imageSize = ScaleHelper.ScaleToInitialSystemDpi(LogicalImageSize);
s_isScalingInitialized = true;
}
for (int i = 0; i < _propertyValueUIItems.Length; i++)
{
_uiItemRects[i] = new Rectangle(
rect.Right - ((s_imageSize + 1) * (i + 1)),
(rect.Height - s_imageSize) / 2,
s_imageSize,
s_imageSize);
g.DrawImage(_propertyValueUIItems[i].Image, _uiItemRects[i]);
}
if (OwnerGridView is { } ownerGridView)
{
ownerGridView.LabelPaintMargin = (s_imageSize + 1) * _propertyValueUIItems.Length;
}
}
private object? SetPropertyValue(
object? owner,
object? value,
bool reset,
string? undoText)
{
DesignerTransaction? transaction = null;
try
{
object? oldValue = GetPropertyValue(owner);
if (value is not null && value.Equals(oldValue))
{
return value;
}
ClearCachedValues();
IDesignerHost? host = DesignerHost;
transaction = host?.CreateTransaction(undoText ?? string.Format(SR.PropertyGridSetValue, PropertyDescriptor.Name));
// Usually IComponent things are sited and this notification will be fired automatically by
// the PropertyDescriptor. However, for non-IComponent sub objects or sub objects that are
// non-sited sub components, we need to manually fire the notification.
bool needChangeNotify = owner is not IComponent component || component.Site is null;
if (needChangeNotify)
{
try
{
if (owner is null)
{
throw new InvalidOperationException();
}
ComponentChangeService?.OnComponentChanging(owner, PropertyDescriptor);
}
catch (CheckoutException coEx)
{
if (coEx == CheckoutException.Canceled)
{
return oldValue;
}
throw;
}
}
bool wasExpanded = InternalExpanded;
int childCount = -1;
if (wasExpanded)
{
childCount = ChildCount;
}
// See if we need to refresh the property browser.
var refresh = PropertyDescriptor.GetAttribute<RefreshPropertiesAttribute>();
bool needsRefresh = wasExpanded || (refresh is not null && !refresh.RefreshProperties.Equals(RefreshProperties.None));
if (needsRefresh)
{
DisposeChildren();
}
// Determine if this is an event being created, and if so, navigate to the event code
EventDescriptor? eventDescriptor = null;
// This is possibly an event. Check it out.
if (owner is not null && value is string)
{
_eventBindings ??= this.GetService<IEventBindingService>();
eventDescriptor = _eventBindings?.GetEvent(PropertyDescriptor);
// For a merged set of properties, the event binding service won't
// find an event. So, we ask the type descriptor directly.
if (eventDescriptor is null)
{
// If we have a merged property descriptor, pull out one of the elements.
object? eventObj = owner;
if (PropertyDescriptor is MergePropertyDescriptor && owner is Array objArray)
{
if (objArray.Length > 0)
{
eventObj = objArray.GetValue(0);
}
}
// TypeDescriptor will return an empty collection if component is null.
eventDescriptor = TypeDescriptor.GetEvents(eventObj!)[PropertyDescriptor.Name];
}
}
bool setSuccessful = false;
try
{
if (reset)
{
if (owner is null)
{
throw new InvalidOperationException();
}
PropertyDescriptor.ResetValue(owner);
}
else if (eventDescriptor is not null)
{
ViewEvent(owner, (string?)value, eventDescriptor, false);
}
else
{
// Not an event
SetPropertyValueCore(owner, value);
}
setSuccessful = true;
// Now notify the change service that the change was successful.
if (needChangeNotify)
{
// We have already checked if owner is null.
ComponentChangeService?.OnComponentChanged(owner!, PropertyDescriptor, oldValue: null, value);
}
NotifyParentsOfChanges(this);
}
finally
{
if (needsRefresh && OwnerGridView is not null)
{
RecreateChildren(childCount);
if (setSuccessful)
{
OwnerGridView.Refresh(refresh is not null && refresh.Equals(RefreshPropertiesAttribute.All));
}
}
}
}
catch (CheckoutException checkoutEx)
{
transaction?.Cancel();
transaction = null;
if (checkoutEx == CheckoutException.Canceled)
{
return null;
}
throw;
}
catch
{
transaction?.Cancel();
transaction = null;
throw;
}
finally
{
transaction?.Commit();
}
return owner;
}
protected void SetPropertyValueCore(object? owner, object? newValue)
{
// Store the current cursor and set it to the HourGlass.
Cursor? oldCursor = Cursor.Current;
try
{
Cursor.Current = Cursors.WaitCursor;
object? realOwner = owner;
if (realOwner is ICustomTypeDescriptor descriptor)
{
realOwner = descriptor.GetPropertyOwner(PropertyDescriptor);
}
// Check the type of the object we are modifying. If it's a value type or an array,
// we need to modify the object and push the value back up to the parent.
bool treatAsValueType = false;
if (ParentGridEntry is not null)
{
Type? propertyType = ParentGridEntry.PropertyType;
if (propertyType is not null)
{
treatAsValueType = propertyType.IsValueType || propertyType.IsArray;
}
}
if (realOwner is not null)
{
PropertyDescriptor.SetValue(realOwner, newValue);
// Since the value that we modified may not be stored by the parent property, we need to push this
// value back into the parent. An example here is Size or Location, which return Point objects that
// are unconnected to the object they relate to. So we modify the Point object and push it back
// into the object we got it from.
if (treatAsValueType)
{
GridEntry? parent = ParentGridEntry;
if (parent is not null && parent.IsValueEditable)
{
parent.PropertyValue = owner;
}
}
}
}
finally
{
// Flip back to the old cursor.
Cursor.Current = oldCursor;
}
}
/// <summary>
/// Navigates code to the given event.
/// </summary>
protected bool ViewEvent(
object? owner,
string? newHandler,
EventDescriptor? eventDescriptor,
bool alwaysNavigate)
{
object? value = GetPropertyValue(owner);
string? handler = value as string;
if (handler is null && value is not null && TypeConverter is not null && TypeConverter.CanConvertTo(typeof(string)))
{
handler = TypeConverter.ConvertToString(value);
}
if (newHandler is null && !string.IsNullOrEmpty(handler))
{
newHandler = handler;
}
else if (handler == newHandler && !string.IsNullOrEmpty(newHandler))
{
return true;
}
var component = owner as IComponent;
if (component is null && PropertyDescriptor is MergePropertyDescriptor)
{
// It's possible that multiple objects are selected, and we're trying to create an event for each of them.
if (owner is Array array && array.Length > 0)
{
component = array.GetValue(0) as IComponent;
}
}
if (component is null)
{
return false;
}
if (PropertyDescriptor.IsReadOnly)
{
return false;
}
if (eventDescriptor is null)
{
_eventBindings ??= this.GetService<IEventBindingService>();
eventDescriptor = _eventBindings?.GetEvent(PropertyDescriptor);
}
IDesignerHost? host = DesignerHost;
DesignerTransaction? transaction = null;
try
{
// This check can cause exceptions if the event has unreferenced dependencies, which we want to catch.
// This must be done before the transaction is started or the commit/cancel will also throw.
if (eventDescriptor is null)
{
throw new InvalidOperationException();
}
if (eventDescriptor.EventType is null)
{
return false;
}
if (host is not null)
{
string componentName = component.Site?.Name ?? component.GetType().Name;
transaction = host.CreateTransaction(string.Format(
SR.WindowsFormsSetEvent,
$"{componentName}.{PropertyName}"));
}
_eventBindings ??= component.Site?.GetService<IEventBindingService>();
newHandler ??= _eventBindings?.CreateUniqueMethodName(component, eventDescriptor);
if (newHandler is not null)
{
// Now walk through all the matching methods to see if this one exists.
// If it doesn't we'll want to show code.
if (_eventBindings is not null)
{
bool methodExists = false;
foreach (string methodName in _eventBindings.GetCompatibleMethods(eventDescriptor))
{
if (newHandler == methodName)
{
methodExists = true;
break;
}
}
if (!methodExists)
{
alwaysNavigate = true;
}
}
try
{
PropertyDescriptor.SetValue(owner, newHandler);
}
catch (InvalidOperationException ex)
{
transaction?.Cancel();
transaction = null;
OwnerGridView?.ShowInvalidMessage(ex);
return false;
}
}
if (alwaysNavigate && _eventBindings is not null)
{
s_targetBindingService = _eventBindings;
s_targetComponent = component;
s_targetEventdesc = eventDescriptor;
Application.Idle += ShowCodeIdle;
}
}
catch
{
transaction?.Cancel();
transaction = null;
throw;
}
finally
{
transaction?.Commit();
}
return true;
}
/// <summary>
/// Displays the user code for the given event. This will return true if the user
/// code could be displayed, or false otherwise.
/// </summary>
private static void ShowCodeIdle(object? sender, EventArgs e)
{
Application.Idle -= ShowCodeIdle;
if (s_targetBindingService is not null)
{
// We set s_targetComponent and s_targetEventdesc with s_targetBindingService,
// so we don't expect them to be null here.
s_targetBindingService.ShowCode(s_targetComponent!, s_targetEventdesc!);
s_targetBindingService = null;
s_targetComponent = null;
s_targetEventdesc = null;
}
}
protected override GridEntryAccessibleObject GetAccessibilityObject() =>
new PropertyDescriptorGridEntryAccessibleObject(this);
}
|