2018-06-29 19:52:40 +02:00
using System ;
using System.Collections.Generic ;
using System.Collections.Specialized ;
using System.Diagnostics ;
using System.Linq ;
using System.Reflection ;
using System.Runtime.Serialization ;
using Umbraco.Core.Exceptions ;
using Umbraco.Core.Models.Entities ;
namespace Umbraco.Core.Models
{
/// <summary>
/// Represents an abstract class for base Content properties and methods
/// </summary>
[Serializable]
[DataContract(IsReference = true)]
[DebuggerDisplay("Id: {Id}, Name: {Name}, ContentType: {ContentTypeBase.Alias}")]
public abstract class ContentBase : TreeEntityBase , IContentBase
{
2018-10-23 15:04:41 +02:00
protected static readonly ContentCultureInfosCollection NoInfos = new ContentCultureInfosCollection ();
2018-06-29 19:52:40 +02:00
private static readonly Lazy < PropertySelectors > Ps = new Lazy < PropertySelectors >();
private int _contentTypeId ;
protected IContentTypeComposition ContentTypeBase ;
private int _writerId ;
private PropertyCollection _properties ;
2018-10-23 15:04:41 +02:00
private ContentCultureInfosCollection _cultureInfos ;
2018-06-29 19:52:40 +02:00
/// <summary>
/// Initializes a new instance of the <see cref="ContentBase"/> class.
/// </summary>
protected ContentBase ( string name , int parentId , IContentTypeComposition contentType , PropertyCollection properties , string culture = null )
: this ( name , contentType , properties , culture )
{
if ( parentId == 0 ) throw new ArgumentOutOfRangeException ( nameof ( parentId ));
ParentId = parentId ;
}
/// <summary>
/// Initializes a new instance of the <see cref="ContentBase"/> class.
/// </summary>
protected ContentBase ( string name , IContentBase parent , IContentTypeComposition contentType , PropertyCollection properties , string culture = null )
: this ( name , contentType , properties , culture )
{
if ( parent == null ) throw new ArgumentNullException ( nameof ( parent ));
SetParent ( parent );
}
private ContentBase ( string name , IContentTypeComposition contentType , PropertyCollection properties , string culture = null )
{
ContentTypeBase = contentType ?? throw new ArgumentNullException ( nameof ( contentType ));
// initially, all new instances have
Id = 0 ; // no identity
VersionId = 0 ; // no versions
2018-06-20 14:18:57 +02:00
SetCultureName ( name , culture );
2018-06-29 19:52:40 +02:00
_contentTypeId = contentType . Id ;
_properties = properties ?? throw new ArgumentNullException ( nameof ( properties ));
_properties . EnsurePropertyTypes ( PropertyTypes );
}
// ReSharper disable once ClassNeverInstantiated.Local
private class PropertySelectors
{
public readonly PropertyInfo DefaultContentTypeIdSelector = ExpressionHelper . GetPropertyInfo < ContentBase , int >( x => x . ContentTypeId );
public readonly PropertyInfo PropertyCollectionSelector = ExpressionHelper . GetPropertyInfo < ContentBase , PropertyCollection >( x => x . Properties );
public readonly PropertyInfo WriterSelector = ExpressionHelper . GetPropertyInfo < ContentBase , int >( x => x . WriterId );
2018-10-23 15:04:41 +02:00
public readonly PropertyInfo CultureInfosSelector = ExpressionHelper . GetPropertyInfo < ContentBase , IReadOnlyDictionary < string , ContentCultureInfos >>( x => x . CultureInfos );
2018-06-29 19:52:40 +02:00
}
protected void PropertiesChanged ( object sender , NotifyCollectionChangedEventArgs e )
{
OnPropertyChanged ( Ps . Value . PropertyCollectionSelector );
}
/// <summary>
/// Id of the user who wrote/updated this entity
/// </summary>
[DataMember]
public virtual int WriterId
{
get => _writerId ;
set => SetPropertyValueAndDetectChanges ( value , ref _writerId , Ps . Value . WriterSelector );
}
[IgnoreDataMember]
public int VersionId { get ; internal set ; }
/// <summary>
/// Integer Id of the default ContentType
/// </summary>
[DataMember]
public virtual int ContentTypeId
{
get
{
//There will be cases where this has not been updated to reflect the true content type ID.
//This will occur when inserting new content.
if ( _contentTypeId == 0 && ContentTypeBase != null && ContentTypeBase . HasIdentity )
{
_contentTypeId = ContentTypeBase . Id ;
}
return _contentTypeId ;
}
protected set => SetPropertyValueAndDetectChanges ( value , ref _contentTypeId , Ps . Value . DefaultContentTypeIdSelector );
}
/// <summary>
/// Gets or sets the collection of properties for the entity.
/// </summary>
2018-10-19 08:26:17 +11:00
/// <remarks>
/// Marked DoNotClone since we'll manually clone the underlying field to deal with the event handling
/// </remarks>
2018-06-29 19:52:40 +02:00
[DataMember]
2018-10-19 08:26:17 +11:00
[DoNotClone]
2018-06-29 19:52:40 +02:00
public virtual PropertyCollection Properties
{
get => _properties ;
set
{
_properties = value ;
_properties . CollectionChanged += PropertiesChanged ;
}
}
/// <summary>
/// Gets the enumeration of property groups for the entity.
2019-01-26 09:42:14 -05:00
/// TODO: remove this proxy method
2018-06-29 19:52:40 +02:00
/// </summary>
[IgnoreDataMember]
public IEnumerable < PropertyGroup > PropertyGroups => ContentTypeBase . CompositionPropertyGroups ;
/// <summary>
/// Gets the numeration of property types for the entity.
2019-01-26 09:42:14 -05:00
/// TODO: remove this proxy method
2018-06-29 19:52:40 +02:00
/// </summary>
[IgnoreDataMember]
public IEnumerable < PropertyType > PropertyTypes => ContentTypeBase . CompositionPropertyTypes ;
#region Cultures
2018-06-20 14:18:57 +02:00
// notes - common rules
// - setting a variant value on an invariant content type throws
// - getting a variant value on an invariant content type returns null
// - setting and getting the invariant value is always possible
// - setting a null value clears the value
/// <inheritdoc />
public IEnumerable < string > AvailableCultures
2018-10-17 18:09:52 +11:00
=> _cultureInfos ?. Keys ?? Enumerable . Empty < string >();
2018-06-20 14:18:57 +02:00
/// <inheritdoc />
public bool IsCultureAvailable ( string culture )
2018-10-19 15:41:28 +11:00
=> _cultureInfos != null && _cultureInfos . ContainsKey ( culture );
2018-06-20 14:18:57 +02:00
2018-06-29 19:52:40 +02:00
/// <inheritdoc />
[DataMember]
2018-10-23 15:04:41 +02:00
public virtual IReadOnlyDictionary < string , ContentCultureInfos > CultureInfos => _cultureInfos ?? NoInfos ;
2018-11-13 12:05:02 +01:00
2018-06-29 19:52:40 +02:00
/// <inheritdoc />
2018-10-17 18:09:52 +11:00
public string GetCultureName ( string culture )
2018-06-29 19:52:40 +02:00
{
2018-06-20 14:18:57 +02:00
if ( culture . IsNullOrWhiteSpace ()) return Name ;
if (! ContentTypeBase . VariesByCulture ()) return null ;
2018-06-29 19:52:40 +02:00
if ( _cultureInfos == null ) return null ;
return _cultureInfos . TryGetValue ( culture , out var infos ) ? infos . Name : null ;
}
/// <inheritdoc />
2018-09-25 18:05:14 +02:00
public DateTime ? GetUpdateDate ( string culture )
2018-06-29 19:52:40 +02:00
{
2018-06-20 14:18:57 +02:00
if ( culture . IsNullOrWhiteSpace ()) return null ;
if (! ContentTypeBase . VariesByCulture ()) return null ;
if ( _cultureInfos == null ) return null ;
return _cultureInfos . TryGetValue ( culture , out var infos ) ? infos . Date : ( DateTime ?) null ;
2018-06-29 19:52:40 +02:00
}
2018-06-20 14:18:57 +02:00
/// <inheritdoc />
2018-10-17 18:09:52 +11:00
public void SetCultureName ( string name , string culture )
2018-06-29 19:52:40 +02:00
{
2018-06-20 14:18:57 +02:00
if ( ContentTypeBase . VariesByCulture ()) // set on variant content type
{
if ( culture . IsNullOrWhiteSpace ()) // invariant is ok
{
Name = name ; // may be null
}
else if ( name . IsNullOrWhiteSpace ()) // clear
{
ClearCultureInfo ( culture );
}
else // set
{
SetCultureInfo ( culture , name , DateTime . Now );
}
}
else // set on invariant content type
{
if (! culture . IsNullOrWhiteSpace ()) // invariant is NOT ok
throw new NotSupportedException ( "Content type does not vary by culture." );
2018-06-29 19:52:40 +02:00
2018-06-20 14:18:57 +02:00
Name = name ; // may be null
}
}
protected void ClearCultureInfos ()
{
2018-10-23 15:04:41 +02:00
_cultureInfos ?. Clear ();
2018-06-29 19:52:40 +02:00
_cultureInfos = null ;
}
2018-06-20 14:18:57 +02:00
protected void ClearCultureInfo ( string culture )
2018-06-29 19:52:40 +02:00
{
2018-06-20 14:18:57 +02:00
if ( culture . IsNullOrWhiteSpace ())
throw new ArgumentNullOrEmptyException ( nameof ( culture ));
if ( _cultureInfos == null ) return ;
_cultureInfos . Remove ( culture );
if ( _cultureInfos . Count == 0 )
_cultureInfos = null ;
}
2018-11-13 12:05:02 +01:00
protected void TouchCultureInfo ( string culture )
{
if ( _cultureInfos == null || ! _cultureInfos . TryGetValue ( culture , out var infos )) return ;
_cultureInfos . AddOrUpdate ( culture , infos . Name , DateTime . Now );
}
2018-06-20 14:18:57 +02:00
// internal for repository
internal void SetCultureInfo ( string culture , string name , DateTime date )
{
if ( name . IsNullOrWhiteSpace ())
throw new ArgumentNullOrEmptyException ( nameof ( name ));
if ( culture . IsNullOrWhiteSpace ())
throw new ArgumentNullOrEmptyException ( nameof ( culture ));
if ( _cultureInfos == null )
2018-10-17 18:09:52 +11:00
{
2018-10-23 15:04:41 +02:00
_cultureInfos = new ContentCultureInfosCollection ();
_cultureInfos . CollectionChanged += CultureInfosCollectionChanged ;
2018-11-13 12:05:02 +01:00
}
2018-06-20 14:18:57 +02:00
2018-10-17 18:09:52 +11:00
_cultureInfos . AddOrUpdate ( culture , name , date );
}
/// <summary>
2018-10-23 15:04:41 +02:00
/// Handles culture infos collection changes.
2018-10-17 18:09:52 +11:00
/// </summary>
2018-10-23 15:04:41 +02:00
private void CultureInfosCollectionChanged ( object sender , NotifyCollectionChangedEventArgs e )
2018-10-17 18:09:52 +11:00
{
2018-10-23 15:04:41 +02:00
OnPropertyChanged ( Ps . Value . CultureInfosSelector );
2018-06-29 19:52:40 +02:00
}
#endregion
#region Has , Get , Set , Publish Property Value
/// <inheritdoc />
public virtual bool HasProperty ( string propertyTypeAlias )
=> Properties . Contains ( propertyTypeAlias );
/// <inheritdoc />
public virtual object GetValue ( string propertyTypeAlias , string culture = null , string segment = null , bool published = false )
{
return Properties . TryGetValue ( propertyTypeAlias , out var property )
? property . GetValue ( culture , segment , published )
: null ;
}
/// <inheritdoc />
public virtual TValue GetValue < TValue >( string propertyTypeAlias , string culture = null , string segment = null , bool published = false )
{
if (! Properties . TryGetValue ( propertyTypeAlias , out var property ))
return default ;
var convertAttempt = property . GetValue ( culture , segment , published ). TryConvertTo < TValue >();
return convertAttempt . Success ? convertAttempt . Result : default ;
}
/// <inheritdoc />
public virtual void SetValue ( string propertyTypeAlias , object value , string culture = null , string segment = null )
{
if ( Properties . Contains ( propertyTypeAlias ))
{
Properties [ propertyTypeAlias ]. SetValue ( value , culture , segment );
return ;
}
var propertyType = PropertyTypes . FirstOrDefault ( x => x . Alias . InvariantEquals ( propertyTypeAlias ));
if ( propertyType == null )
throw new InvalidOperationException ( $"No PropertyType exists with the supplied alias \" { propertyTypeAlias } \ "." );
var property = propertyType . CreateProperty ();
property . SetValue ( value , culture , segment );
Properties . Add ( property );
}
#endregion
2018-06-22 21:03:47 +02:00
#region Copy
/// <inheritdoc />
public virtual void CopyFrom ( IContent other , string culture = "*" )
{
if ( other . ContentTypeId != ContentTypeId )
throw new InvalidOperationException ( "Cannot copy values from a different content type." );
culture = culture ?. ToLowerInvariant (). NullOrWhiteSpaceAsNull ();
// the variation should be supported by the content type properties
// if the content type is invariant, only '*' and 'null' is ok
// if the content type varies, everything is ok because some properties may be invariant
if (! ContentTypeBase . SupportsPropertyVariation ( culture , "*" , true ))
throw new NotSupportedException ( $"Culture \" { culture } \ " is not supported by content type \"{ContentTypeBase.Alias}\" with variation \"{ContentTypeBase.Variations}\"." );
// copying from the same Id and VersionPk
var copyingFromSelf = Id == other . Id && VersionId == other . VersionId ;
var published = copyingFromSelf ;
// note: use property.SetValue(), don't assign pvalue.EditValue, else change tracking fails
// clear all existing properties for the specified culture
foreach ( var property in Properties )
{
// each property type may or may not support the variation
if (! property . PropertyType . SupportsVariation ( culture , "*" , wildcards : true ))
continue ;
foreach ( var pvalue in property . Values )
if ( property . PropertyType . SupportsVariation ( pvalue . Culture , pvalue . Segment , wildcards : true ) &&
( culture == "*" || pvalue . Culture . InvariantEquals ( culture )))
{
property . SetValue ( null , pvalue . Culture , pvalue . Segment );
}
}
// copy properties from 'other'
var otherProperties = other . Properties ;
foreach ( var otherProperty in otherProperties )
{
if (! otherProperty . PropertyType . SupportsVariation ( culture , "*" , wildcards : true ))
continue ;
var alias = otherProperty . PropertyType . Alias ;
foreach ( var pvalue in otherProperty . Values )
{
if ( otherProperty . PropertyType . SupportsVariation ( pvalue . Culture , pvalue . Segment , wildcards : true ) &&
( culture == "*" || pvalue . Culture . InvariantEquals ( culture )))
{
var value = published ? pvalue . PublishedValue : pvalue . EditedValue ;
SetValue ( alias , value , pvalue . Culture , pvalue . Segment );
}
}
}
// copy names, too
if ( culture == "*" )
ClearCultureInfos ();
if ( culture == null || culture == "*" )
Name = other . Name ;
2018-10-23 15:04:41 +02:00
foreach ( var ( otherCulture , otherInfos ) in other . CultureInfos )
2018-06-22 21:03:47 +02:00
{
if ( culture == "*" || culture == otherCulture )
2018-10-23 15:04:41 +02:00
SetCultureName ( otherInfos . Name , otherCulture );
2018-06-22 21:03:47 +02:00
}
}
#endregion
2018-06-29 19:52:40 +02:00
#region Validation
2018-11-13 12:05:02 +01:00
2018-06-29 19:52:40 +02:00
/// <inheritdoc />
2019-02-04 16:55:35 +11:00
public Property [] ValidateProperties ( string culture = "*" )
2018-06-29 19:52:40 +02:00
{
2018-06-22 21:03:47 +02:00
var alsoInvariant = culture != null && culture != "*" ;
2018-06-20 14:18:57 +02:00
return Properties . Where ( x => // select properties...
2018-06-22 21:03:47 +02:00
x . PropertyType . SupportsVariation ( culture , "*" , true ) && // that support the variation
(! x . IsValid ( culture ) || ( alsoInvariant && ! x . IsValid ( null )))) // and are not valid
2018-06-20 14:18:57 +02:00
. ToArray ();
2018-06-29 19:52:40 +02:00
}
#endregion
#region Dirty
/// <inheritdoc />
2019-01-22 18:03:39 -05:00
/// <remarks>Overridden to include user properties.</remarks>
2018-06-29 19:52:40 +02:00
public override void ResetDirtyProperties ( bool rememberDirty )
{
base . ResetDirtyProperties ( rememberDirty );
// also reset dirty changes made to user's properties
foreach ( var prop in Properties )
prop . ResetDirtyProperties ( rememberDirty );
2018-10-17 18:09:52 +11:00
2018-10-23 15:04:41 +02:00
// take care of culture infos
if ( _cultureInfos == null ) return ;
foreach ( var cultureInfo in _cultureInfos )
cultureInfo . ResetDirtyProperties ( rememberDirty );
2018-06-29 19:52:40 +02:00
}
/// <inheritdoc />
2019-01-22 18:03:39 -05:00
/// <remarks>Overridden to include user properties.</remarks>
2018-06-29 19:52:40 +02:00
public override bool IsDirty ()
{
return IsEntityDirty () || this . IsAnyUserPropertyDirty ();
}
/// <inheritdoc />
2019-01-22 18:03:39 -05:00
/// <remarks>Overridden to include user properties.</remarks>
2018-06-29 19:52:40 +02:00
public override bool WasDirty ()
{
return WasEntityDirty () || this . WasAnyUserPropertyDirty ();
}
/// <summary>
/// Gets a value indicating whether the current entity's own properties (not user) are dirty.
/// </summary>
public bool IsEntityDirty ()
{
return base . IsDirty ();
}
/// <summary>
/// Gets a value indicating whether the current entity's own properties (not user) were dirty.
/// </summary>
public bool WasEntityDirty ()
{
return base . WasDirty ();
}
/// <inheritdoc />
2019-01-22 18:03:39 -05:00
/// <remarks>Overridden to include user properties.</remarks>
2018-06-29 19:52:40 +02:00
public override bool IsPropertyDirty ( string propertyName )
{
if ( base . IsPropertyDirty ( propertyName ))
return true ;
return Properties . Contains ( propertyName ) && Properties [ propertyName ]. IsDirty ();
}
/// <inheritdoc />
2019-01-22 18:03:39 -05:00
/// <remarks>Overridden to include user properties.</remarks>
2018-06-29 19:52:40 +02:00
public override bool WasPropertyDirty ( string propertyName )
{
if ( base . WasPropertyDirty ( propertyName ))
return true ;
return Properties . Contains ( propertyName ) && Properties [ propertyName ]. WasDirty ();
}
/// <inheritdoc />
2019-01-22 18:03:39 -05:00
/// <remarks>Overridden to include user properties.</remarks>
2018-06-29 19:52:40 +02:00
public override IEnumerable < string > GetDirtyProperties ()
{
var instanceProperties = base . GetDirtyProperties ();
var propertyTypes = Properties . Where ( x => x . IsDirty ()). Select ( x => x . Alias );
return instanceProperties . Concat ( propertyTypes );
}
/// <inheritdoc />
2019-01-22 18:03:39 -05:00
/// <remarks>Overridden to include user properties.</remarks>
2018-06-29 19:52:40 +02:00
public override IEnumerable < string > GetWereDirtyProperties ()
{
var instanceProperties = base . GetWereDirtyProperties ();
var propertyTypes = Properties . Where ( x => x . WasDirty ()). Select ( x => x . Alias );
return instanceProperties . Concat ( propertyTypes );
}
#endregion
2018-10-19 08:26:17 +11:00
2018-10-23 15:04:41 +02:00
/// <inheritdoc />
/// <remarks>
2019-01-22 18:03:39 -05:00
/// Overridden to deal with specific object instances
2018-10-23 15:04:41 +02:00
/// </remarks>
2018-10-25 15:47:25 +11:00
protected override void PerformDeepClone ( object clone )
2018-10-19 08:26:17 +11:00
{
2018-10-25 15:47:25 +11:00
base . PerformDeepClone ( clone );
2018-10-23 15:04:41 +02:00
2018-10-25 15:47:25 +11:00
var clonedContent = ( ContentBase ) clone ;
2018-10-19 08:26:17 +11:00
//if culture infos exist then deal with event bindings
2018-10-25 15:47:25 +11:00
if ( clonedContent . _cultureInfos != null )
2018-10-19 08:26:17 +11:00
{
2018-10-25 15:47:25 +11:00
clonedContent . _cultureInfos . CollectionChanged -= CultureInfosCollectionChanged ; //clear this event handler if any
clonedContent . _cultureInfos = ( ContentCultureInfosCollection ) _cultureInfos . DeepClone (); //manually deep clone
clonedContent . _cultureInfos . CollectionChanged += clonedContent . CultureInfosCollectionChanged ; //re-assign correct event handler
2018-10-19 08:26:17 +11:00
}
//if properties exist then deal with event bindings
2018-10-25 15:47:25 +11:00
if ( clonedContent . _properties != null )
2018-10-19 08:26:17 +11:00
{
2018-10-25 15:47:25 +11:00
clonedContent . _properties . CollectionChanged -= PropertiesChanged ; //clear this event handler if any
clonedContent . _properties = ( PropertyCollection ) _properties . DeepClone (); //manually deep clone
clonedContent . _properties . CollectionChanged += clonedContent . PropertiesChanged ; //re-assign correct event handler
2018-10-19 08:26:17 +11:00
}
}
2018-06-29 19:52:40 +02:00
}
}