WpfDesigner/WpfDesign.XamlDom/Project/XamlProperty.cs

733 строки
22 KiB
C#

// Copyright (c) 2019 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Xml;
using System.Windows;
using System.Windows.Markup;
using System.Xaml;
namespace ICSharpCode.WpfDesign.XamlDom
{
/// <summary>
/// Describes a property on a <see cref="XamlObject"/>.
/// </summary>
[DebuggerDisplay("XamlProperty: {PropertyName}")]
public sealed class XamlProperty
{
XamlObject parentObject;
internal readonly XamlPropertyInfo propertyInfo;
XamlPropertyValue propertyValue;
CollectionElementsCollection collectionElements;
bool isCollection;
bool isResources;
static readonly IList<XamlPropertyValue> emptyCollectionElementsArray = new XamlPropertyValue[0];
// for use by parser only
internal XamlProperty(XamlObject parentObject, XamlPropertyInfo propertyInfo, XamlPropertyValue propertyValue)
: this(parentObject, propertyInfo)
{
PossiblyNameChanged(null, propertyValue);
this.propertyValue = propertyValue;
if (propertyValue != null) {
propertyValue.ParentProperty = this;
}
UpdateValueOnInstance();
}
internal XamlProperty(XamlObject parentObject, XamlPropertyInfo propertyInfo)
{
this.parentObject = parentObject;
this.propertyInfo = propertyInfo;
if (propertyInfo.IsCollection) {
isCollection = true;
collectionElements = new CollectionElementsCollection(this);
collectionElements.CollectionChanged += OnCollectionChanged;
if (propertyInfo.Name.Equals(XamlConstants.ResourcesPropertyName, StringComparison.Ordinal) &&
propertyInfo.ReturnType == typeof(ResourceDictionary)) {
isResources = true;
}
}
}
void OnCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
// If implicit collection that is now empty we remove markup for the property if still there.
if (collectionElements.Count == 0 && propertyValue == null && _propertyElement != null)
{
_propertyElement.ParentNode.RemoveChild(_propertyElement);
_propertyElement = null;
ParentObject.OnPropertyChanged(this);
if (IsSetChanged != null) {
IsSetChanged(this, EventArgs.Empty);
}
if (ValueChanged != null) {
ValueChanged(this, EventArgs.Empty);
}
}
}
/// <summary>
/// Gets the parent object for which this property was declared.
/// </summary>
public XamlObject ParentObject {
get { return parentObject; }
}
/// <summary>
/// Gets the property name.
/// </summary>
public string PropertyName {
get { return propertyInfo.Name; }
}
/// <summary>
/// Gets the type the property is declared on.
/// </summary>
public Type PropertyTargetType {
get { return propertyInfo.TargetType; }
}
/// <summary>
/// Gets if this property is an attached property.
/// </summary>
public bool IsAttached {
get { return propertyInfo.IsAttached; }
}
/// <summary>
/// Gets if this property is an event.
/// </summary>
public bool IsEvent {
get { return propertyInfo.IsEvent; }
}
/// <summary>
/// Gets the return type of the property.
/// </summary>
public Type ReturnType {
get { return propertyInfo.ReturnType; }
}
/// <summary>
/// Gets the type converter used to convert property values to/from string.
/// </summary>
public TypeConverter TypeConverter {
get { return propertyInfo.TypeConverter; }
}
/// <summary>
/// Gets the category of the property.
/// </summary>
public string Category {
get { return propertyInfo.Category; }
}
/// <summary>
/// Gets the value of the property. Can be null if the property is a collection property.
/// </summary>
public XamlPropertyValue PropertyValue {
get { return propertyValue; }
set { SetPropertyValue(value); }
}
/// <summary>
/// Gets if the property represents the FrameworkElement.Resources property that holds a locally-defined resource dictionary.
/// </summary>
public bool IsResources {
get { return isResources; }
}
/// <summary>
/// Gets if the property is a collection property.
/// </summary>
public bool IsCollection {
get { return isCollection; }
}
/// <summary>
/// Gets the collection elements of the property. Is empty if the property is not a collection.
/// </summary>
public IList<XamlPropertyValue> CollectionElements {
get { return collectionElements ?? emptyCollectionElementsArray; }
}
/// <summary>
/// Gets if the property is set.
/// </summary>
public bool IsSet {
get { return propertyValue != null ||
_propertyElement != null; // collection
}
}
/// <summary>
/// Occurs when the value of the IsSet property has changed.
/// </summary>
public event EventHandler IsSetChanged;
/// <summary>
/// Occurs when the value of the property has changed.
/// </summary>
public event EventHandler ValueChanged;
/// <summary>
/// Occurs when MarkupExtension evaluated PropertyValue dosn't changed but ValueOnInstance does.
/// </summary>
public event EventHandler ValueOnInstanceChanged;
void SetPropertyValue(XamlPropertyValue value)
{
// Binding...
//if (IsCollection) {
// throw new InvalidOperationException("Cannot set the value of collection properties.");
//}
bool wasSet = this.IsSet;
PossiblyNameChanged(propertyValue, value);
//reset expression
var xamlObject = propertyValue as XamlObject;
if (xamlObject != null && xamlObject.IsMarkupExtension)
propertyInfo.ResetValue(parentObject.Instance);
ResetInternal();
propertyValue = value;
if (propertyValue != null) {
propertyValue.ParentProperty = this;
propertyValue.AddNodeTo(this);
}
UpdateValueOnInstance();
ParentObject.OnPropertyChanged(this);
if (!wasSet) {
if (IsSetChanged != null) {
IsSetChanged(this, EventArgs.Empty);
}
}
if (ValueChanged != null) {
ValueChanged(this, EventArgs.Empty);
}
}
internal void UpdateValueOnInstance()
{
if (PropertyValue != null) {
try {
ValueOnInstance = PropertyValue.GetValueFor(propertyInfo);
if (this.parentObject.XamlSetTypeConverter != null && propertyValue is XamlTextValue) {
this.ParentObject.XamlSetTypeConverter(this.parentObject.Instance, new XamlSetTypeConverterEventArgs(this.SystemXamlMemberForProperty, null, ((XamlTextValue) propertyValue).Text, this.parentObject.OwnerDocument.GetTypeDescriptorContext(this.parentObject), null));
}
if (propertyInfo.DependencyProperty == DesignTimeProperties.DesignWidthProperty) {
var widthProperty = this.ParentObject.Properties.FirstOrDefault(x => x.DependencyProperty == FrameworkElement.WidthProperty);
if (widthProperty == null || !widthProperty.IsSet)
((FrameworkElement) this.ParentObject.Instance).Width = (double) ValueOnInstance;
}
if (propertyInfo.DependencyProperty == DesignTimeProperties.DesignHeightProperty) {
var heightProperty = this.ParentObject.Properties.FirstOrDefault(x => x.DependencyProperty == FrameworkElement.HeightProperty);
if (heightProperty == null || !heightProperty.IsSet)
((FrameworkElement)this.ParentObject.Instance).Height = (double)ValueOnInstance;
}
}
catch (Exception ex) {
Debug.WriteLine("UpdateValueOnInstance() failed - Exception:" + ex.Message);
}
} else if (IsCollection) {
var list = ValueOnInstance as System.Collections.IList;
if (list != null) {
list.Clear();
foreach (var item in CollectionElements) {
var newValue = item.GetValueFor(propertyInfo);
list.Add(newValue);
}
}
}
}
private XamlMember _systemXamlMemberForProperty = null;
/// <summary>
/// Gets a <see cref="XamlMember"/> representing the property.
/// </summary>
public XamlMember SystemXamlMemberForProperty
{
get
{
if (_systemXamlMemberForProperty == null)
_systemXamlMemberForProperty = new XamlMember(this.PropertyName, SystemXamlTypeForProperty, false);
return _systemXamlMemberForProperty;
}
}
private XamlType _systemXamlTypeForProperty = null;
/// <summary>
/// Gets a <see cref="XamlType"/> representing the type the property is declared on.
/// </summary>
public XamlType SystemXamlTypeForProperty
{
get
{
if (_systemXamlTypeForProperty == null)
_systemXamlTypeForProperty = new XamlType(this.PropertyTargetType,
this.ParentObject.ServiceProvider.SchemaContext);
return _systemXamlTypeForProperty;
}
}
/// <summary>
/// Resets the properties value.
/// </summary>
public void Reset()
{
if (IsSet) {
propertyInfo.ResetValue(parentObject.Instance);
ResetInternal();
ParentObject.OnPropertyChanged(this);
if (IsSetChanged != null) {
IsSetChanged(this, EventArgs.Empty);
}
if (ValueChanged != null) {
ValueChanged(this, EventArgs.Empty);
}
}
}
void ResetInternal()
{
bool isExplicitCollection = false;
if (propertyValue != null) {
isExplicitCollection = IsCollection;
propertyValue.RemoveNodeFromParent();
propertyValue.ParentProperty = null;
propertyValue = null;
}
if (_propertyElement != null) {
Debug.Assert(!isExplicitCollection || _propertyElement.ParentNode == null);
if (!isExplicitCollection) {
_propertyElement.ParentNode.RemoveChild(_propertyElement);
}
_propertyElement = null;
}
}
XmlElement _propertyElement;
internal void ParserSetPropertyElement(XmlElement propertyElement)
{
XmlElement oldPropertyElement = _propertyElement;
if (oldPropertyElement == propertyElement) return;
_propertyElement = propertyElement;
if (oldPropertyElement != null && IsCollection) {
Debug.WriteLine("Property element for " + this.PropertyName + " already exists, merging..");
foreach (XamlPropertyValue val in this.collectionElements) {
val.RemoveNodeFromParent();
val.AddNodeTo(this);
}
oldPropertyElement.ParentNode.RemoveChild(oldPropertyElement);
}
}
bool IsFirstChildResources(XamlObject obj)
{
return obj.XmlElement.FirstChild != null &&
obj.XmlElement.FirstChild.Name.EndsWith("." + XamlConstants.ResourcesPropertyName) &&
obj.Properties.Where((prop) => prop.IsResources).FirstOrDefault() != null;
}
XmlElement CreatePropertyElement()
{
Type propertyElementType = GetPropertyElementType();
string ns = parentObject.OwnerDocument.GetNamespaceFor(propertyElementType);
return parentObject.OwnerDocument.XmlDocument.CreateElement(
parentObject.OwnerDocument.GetPrefixForNamespace(ns),
propertyElementType.Name + "." + this.PropertyName,
ns
);
}
Type GetPropertyElementType()
{
return this.IsAttached ? this.PropertyTargetType : parentObject.ElementType;
}
static XmlNode FindChildNode(XmlNode node, Type elementType, string propertyName, XamlDocument xamlDocument)
{
var localName = elementType.Name + "." + propertyName;
var namespacesURI = xamlDocument.GetNamespacesFor(elementType);
var clrNamespaceURI = xamlDocument.GetNamespaceFor(elementType, true);
foreach (XmlNode childNode in node.ChildNodes)
{
if (childNode.LocalName == localName && (namespacesURI.Contains(childNode.NamespaceURI) || childNode.NamespaceURI == clrNamespaceURI))
{
return childNode;
}
}
var type = elementType.BaseType;
namespacesURI = xamlDocument.GetNamespacesFor(type);
while (type != typeof(object))
{
if (type.GetProperty(propertyName) == null)
break;
localName = type.Name + "." + propertyName;
foreach (XmlNode childNode in node.ChildNodes)
{
if (childNode.LocalName == localName && namespacesURI.Contains(childNode.NamespaceURI))
{
return childNode;
}
}
type = type.BaseType;
}
return null;
}
bool IsNodeCollectionForThisProperty(XmlNode node)
{
//Remove the commented check! This is Possible: BeginStoryboard=>The COntent Property is Storyboard, and the Content Element is also Storyboard!
return _propertyElement == null /* && this.PropertyName != this.ParentObject.ContentPropertyName */ && this.ReturnType.IsAssignableFrom(this.ParentObject.OwnerDocument.TypeFinder.GetType(node.NamespaceURI, node.LocalName));
}
internal void AddChildNodeToProperty(XmlNode newChildNode)
{
if (this.IsCollection) {
if (IsNodeCollectionForThisProperty(newChildNode)) {
Type propertyElementType = GetPropertyElementType();
XmlNode parentNode = FindChildNode(parentObject.XmlElement, propertyElementType, this.PropertyName, parentObject.OwnerDocument);
if (parentNode == null) {
parentNode = CreatePropertyElement();
parentObject.XmlElement.AppendChild(parentNode);
}
else if (parentNode.ChildNodes.Cast<XmlNode>().Where(x => !(x is XmlWhitespace)).Count() > 0)
throw new XamlLoadException("Collection property node must have no children when adding collection element.");
parentNode.AppendChild(newChildNode);
_propertyElement = (XmlElement)newChildNode;
}
else {
// this is the default collection
InsertNodeInCollection(newChildNode, collectionElements.Count);
}
return;
}
if (_propertyElement == null) {
if (PropertyName == parentObject.ContentPropertyName) {
if (IsFirstChildResources(parentObject)) {
// Resources element should always be first
parentObject.XmlElement.InsertAfter(newChildNode, parentObject.XmlElement.FirstChild);
}
else
parentObject.XmlElement.InsertBefore(newChildNode, parentObject.XmlElement.FirstChild);
return;
}
_propertyElement = CreatePropertyElement();
if (IsFirstChildResources(parentObject)) {
// Resources element should always be first
parentObject.XmlElement.InsertAfter(_propertyElement, parentObject.XmlElement.FirstChild);
}
else
parentObject.XmlElement.InsertBefore(_propertyElement, parentObject.XmlElement.FirstChild);
}
_propertyElement.AppendChild(newChildNode);
}
internal void InsertNodeInCollection(XmlNode newChildNode, int index)
{
Debug.Assert(index >= 0 && index <= collectionElements.Count);
XmlElement collection = _propertyElement;
if (collection == null) {
if (collectionElements.Count == 0 && this.PropertyName != this.ParentObject.ContentPropertyName) {
// we have to create the collection element
_propertyElement = CreatePropertyElement();
if (this.IsResources) {
parentObject.XmlElement.PrependChild(_propertyElement);
} else {
parentObject.XmlElement.AppendChild(_propertyElement);
}
collection = _propertyElement;
} else {
// this is the default collection
collection = parentObject.XmlElement;
}
}
if (collectionElements.Count == 0) {
// collection is empty -> we may insert anywhere
collection.AppendChild(newChildNode);
} else if (index == collectionElements.Count) {
// insert after last element in collection
collection.InsertAfter(newChildNode, collectionElements[collectionElements.Count - 1].GetNodeForCollection());
} else {
// insert before specified index
collection.InsertBefore(newChildNode, collectionElements[index].GetNodeForCollection());
}
}
internal XmlAttribute SetAttribute(string value)
{
string name;
var element = ParentObject.XmlElement;
if (IsAttached)
{
if (PropertyTargetType == typeof(DesignTimeProperties)
|| PropertyTargetType == typeof(MarkupCompatibilityProperties)
|| PropertyTargetType == typeof(XamlNamespaceProperties))
name = PropertyName;
else
name = PropertyTargetType.Name + "." + PropertyName;
string ns = ParentObject.OwnerDocument.GetNamespaceFor(PropertyTargetType);
string prefix = element.GetPrefixOfNamespace(ns);
if (String.IsNullOrEmpty(prefix)) {
prefix = ParentObject.OwnerDocument.GetPrefixForNamespace(ns);
}
var existingNameSpace = element.GetNamespaceOfPrefix(prefix);
if (!string.IsNullOrEmpty(prefix) || existingNameSpace != ns) {
element.SetAttribute(name, ns, value);
return element.GetAttributeNode(name, ns);
}
} else {
name = PropertyName;
}
element.SetAttribute(name, string.Empty, value);
return element.GetAttributeNode(name);
}
internal string GetNameForMarkupExtension()
{
if (IsAttached) {
string name = PropertyTargetType.Name + "." + PropertyName;
var element = ParentObject.XmlElement;
string ns = ParentObject.OwnerDocument.GetNamespaceFor(PropertyTargetType);
var prefix = element.GetPrefixOfNamespace(ns);
if (string.IsNullOrEmpty(prefix))
return name;
else
return prefix + ":" + name;
} else
return PropertyName;
}
/// <summary>
/// used internally by the XamlParser.
/// Add a collection element that already is part of the XML DOM.
/// </summary>
internal void ParserAddCollectionElement(XmlElement collectionPropertyElement, XamlPropertyValue val)
{
if (collectionPropertyElement != null && _propertyElement == null) {
ParserSetPropertyElement(collectionPropertyElement);
}
collectionElements.AddInternal(val);
val.ParentProperty = this;
if (collectionPropertyElement != _propertyElement) {
val.RemoveNodeFromParent();
val.AddNodeTo(this);
}
}
/// <summary>
/// Gets/Sets the value of the property on the instance without updating the XAML document.
/// </summary>
public object ValueOnInstance {
get {
if (IsEvent) {
if (propertyValue != null)
return propertyValue.GetValueFor(null);
else
return null;
} else {
var value = propertyInfo.GetValue(parentObject.Instance);
return value;
}
}
set {
var setValue = value;
if (propertyInfo.ReturnType == typeof(Uri))
{
setValue = this.ParentObject.OwnerDocument.TypeFinder.ConvertUriToLocalUri((Uri)value);
}
propertyInfo.SetValue(parentObject.Instance, setValue);
if (ValueOnInstanceChanged != null)
ValueOnInstanceChanged(this, EventArgs.Empty);
}
}
/// <summary>
/// Gets/Sets the value of the property in the designer without updating the XAML document.
/// </summary>
public object DesignerValue {
get {
if (IsEvent) {
if (propertyValue != null)
return propertyValue.GetValueFor(null);
else
return null;
} else {
try {
if (propertyValue != null) {
var wr = propertyValue.GetValueFor(this.propertyInfo);
return wr;
}
}
catch(Exception)
{ }
var value = propertyInfo.GetValue(parentObject.Instance);
return value;
}
}
set { ValueOnInstance = value; }
}
/// <summary>
/// Gets the value of the text property on the instance without updating the XAML document.
/// </summary>
public string TextValueOnInstance
{
get
{
if (propertyValue is XamlTextValue)
return propertyValue.GetValueFor(null) as string;
return null;
}
}
/// <summary>
/// Gets if this property is considered "advanced" and should be hidden by default in a property grid.
/// </summary>
public bool IsAdvanced {
get { return propertyInfo.IsAdvanced; }
}
/// <summary>
/// Gets the dependency property.
/// </summary>
public DependencyProperty DependencyProperty {
get {
return propertyInfo.DependencyProperty;
}
}
void PossiblyNameChanged(XamlPropertyValue oldValue, XamlPropertyValue newValue)
{
if (ParentObject.RuntimeNameProperty != null && PropertyName == ParentObject.RuntimeNameProperty) {
if (!String.IsNullOrEmpty(ParentObject.GetXamlAttribute("Name"))) {
throw new XamlLoadException("The property 'Name' is set more than once.");
}
string oldName = null;
string newName = null;
var oldTextValue = oldValue as XamlTextValue;
if (oldTextValue != null) oldName = oldTextValue.Text;
var newTextValue = newValue as XamlTextValue;
if (newTextValue != null) newName = newTextValue.Text;
NameScopeHelper.NameChanged(ParentObject, oldName, newName);
}
}
/*public bool IsAttributeSyntax {
get {
return attribute != null;
}
}
public bool IsElementSyntax {
get {
return element != null;
}
}
public bool IsImplicitDefaultProperty {
get {
return attribute == null && element == null;
}
}*/
/// <inheritdoc />
public T GetValueOnInstance<T>()
{
var obj = ValueOnInstance;
if (obj == null)
{
return default;
}
if (obj is T typed)
{
return typed;
}
if (TypeConverter.CanConvertTo(typeof(T)))
{
return (T)TypeConverter.ConvertTo(obj, typeof(T));
}
return (T)obj;
}
}
}