/// <summary>
        /// Applies any changes detected by a previous call to
        /// <see cref="DetectChanges(StructuredContentHelper, string)"/> to the
        /// database. This method does not persist any changes to the database,
        /// you must still call <see cref="DbContext.SaveChanges()"/>.
        /// </summary>
        /// <param name="helper">The content helper.</param>
        /// <param name="changes">The changes that were returned by a previous call to DetectChanges()." />.</param>
        /// <param name="rockContext">The rock database context to use when saving changes.</param>
        /// <returns><c>true</c> if any changes were made that require you to call <see cref="DbContext.SaveChanges()"/>; otherwise <c>false</c>.</returns>
        public static bool ApplyDatabaseChanges(this StructuredContentHelper helper, StructuredContentChanges changes, RockContext rockContext)
        {
            if (changes == null)
            {
                throw new ArgumentNullException(nameof(changes));
            }

            if (rockContext == null)
            {
                throw new ArgumentNullException(nameof(rockContext));
            }

            return(ApplyDatabaseChanges(helper, changes, rockContext, GetBlockChangeHandlers()));
        }
        /// <summary>
        /// Detects the changes that need to be applied to the database by
        /// looking at the old content and the current content.
        /// </summary>
        /// <param name="helper">The content helper.</param>
        /// <param name="oldContent">The old structured content before the save.</param>
        /// <param name="changeHandlers">The change handlers to use for detection.</param>
        /// <returns>
        /// The changes that were detected.
        /// </returns>
        /// <remarks>This method is internal so that it can be used for unit testing.</remarks>
        internal static StructuredContentChanges DetectChanges(this StructuredContentHelper helper, string oldContent, IReadOnlyDictionary <string, IReadOnlyList <IStructuredContentBlockChangeHandler> > changeHandlers)
        {
            var changes = new StructuredContentChanges();
            var newData = helper.Content?.FromJsonOrNull <StructuredContentData>() ?? new StructuredContentData();
            var oldData = oldContent?.FromJsonOrNull <StructuredContentData>() ?? new StructuredContentData();

            // Walk all the blocks that already existed and still exist or
            // are new in the data.
            foreach (var newBlock in newData.Blocks)
            {
                if (changeHandlers.TryGetValue(newBlock.Type, out var handlers))
                {
                    var oldBlock = oldData.Blocks.FirstOrDefault(b => b.Id == newBlock.Id);

                    foreach (var handler in handlers)
                    {
                        handler.DetectChanges(newBlock.Data, oldBlock?.Data, changes);
                    }
                }
            }

            // Walk all the old blocks that no longer exist.
            var newBlockIds = newData.Blocks.Select(b => b.Id).ToList();

            foreach (var removedBlock in oldData.Blocks.Where(b => !newBlockIds.Contains(b.Id)))
            {
                if (changeHandlers.TryGetValue(removedBlock.Type, out var handlers))
                {
                    foreach (var handler in handlers)
                    {
                        handler.DetectChanges(null, removedBlock.Data, changes);
                    }
                }
            }

            return(changes);
        }
        /// <summary>
        /// Applies any changes detected by a previous call to
        /// <see cref="DetectChanges(StructuredContentHelper, string)"/> to the
        /// database. This method does not persist any changes to the database,
        /// you must still call <see cref="DbContext.SaveChanges()"/>.
        /// </summary>
        /// <param name="helper">The content helper.</param>
        /// <param name="changes">The changes that were returned by a previous call to DetectChanges()." />.</param>
        /// <param name="rockContext">The rock database context to use when saving changes.</param>
        /// <param name="changeHandlers">The change handlers to use for applying changes.</param>
        /// <returns><c>true</c> if any changes were made that require you to call <see cref="DbContext.SaveChanges()"/>; otherwise <c>false</c>.</returns>
        /// <remarks>This method is internal so that it can be used for unit testing.</remarks>
        internal static bool ApplyDatabaseChanges(this StructuredContentHelper helper, StructuredContentChanges changes, RockContext rockContext, IReadOnlyDictionary <string, IReadOnlyList <IStructuredContentBlockChangeHandler> > changeHandlers)
        {
            bool needSave = false;

            // Notify each change handler about the change.
            foreach (var handler in changeHandlers.SelectMany(a => a.Value))
            {
                if (handler.ApplyDatabaseChanges(helper, changes, rockContext))
                {
                    needSave = true;
                }
            }

            return(needSave);
        }
 /// <inheritdoc/>
 public virtual bool ApplyDatabaseChanges(StructuredContentHelper helper, StructuredContentChanges changes, RockContext rockContext)
 {
     return(false);
 }
 /// <inheritdoc cref="IStructuredContentBlockChangeHandler.DetectChanges(dynamic, dynamic, StructuredContentChanges)"/>
 protected virtual void DetectBlockChanges(TData newData, TData oldData, StructuredContentChanges changes)
 {
 }
        /// <inheritdoc/>
        void IStructuredContentBlockChangeHandler.DetectChanges(dynamic newData, dynamic oldData, StructuredContentChanges changes)
        {
            TData oldBlockData, newBlockData;

            if (newData == null)
            {
                newBlockData = null;
            }
            else if (newData is Newtonsoft.Json.Linq.JToken newToken)
            {
                newBlockData = newToken.ToObject <TData>();
            }
            else
            {
                throw new ArgumentException("Expected a JToken object.", nameof(newData));
            }

            if (oldData == null)
            {
                oldBlockData = null;
            }
            else if (oldData is Newtonsoft.Json.Linq.JToken oldToken)
            {
                oldBlockData = oldToken.ToObject <TData>();
            }
            else
            {
                throw new ArgumentException("Expected a JToken object.", nameof(oldData));
            }

            DetectBlockChanges(newBlockData, oldBlockData, changes);
        }