Friday, September 25, 2009

buffering linq changes for row at a time submits - revisited

So I've reworked this thing entirely. The old way still did not entirely allow for the local cache and db staying in sync properly.

The new solution would prefer the primary (or global or main) DataContext to have objectTrackingEnabled set to false. So that it doesn't also cache a copy of the object we are buffering. This means that lazy loading will be off, but you can eager load the necessary items. In most apps I've written so far there's only one major table that needs row level save/rollbacks anyhow.

Pros:
enables row level rollback, customized delete actions (logical deletes, delete recording, etc..)
Only really needed/useful for items or changes that you would like row-level rollback/editing.
includes the conditional move functionality( checks for errors, lets the user know if it can't save, asks if they want to save, gives the option to cancel the movement because of errors, or they don't remember if they want to save or not.)

cons:
main/global datacontext should have objectTrackingEnabled=false or code should not edit the entities that are wrapped in this objectType.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Diagnostics;
using System.Windows.Forms;

namespace BReusable
{
    /// <summary>
    ///
    /// </summary>
    /// <typeparam name="TContext"></typeparam>
    /// <typeparam name="TEntity"></typeparam>
    /// <remarks> State: _readonlyEntity and _LiveEntity are the same means new entity that is not attached
    /// _readonlyEntity !=null and _LiveEntity=null; object that has not been edited.</remarks>
    public abstract class BufferedLinqEntity2<TContext, TEntity>
        where TContext : System.Data.Linq.DataContext
        where TEntity : class
    {
        /// <summary>
        /// Should never be null
        /// </summary>
        TEntity _readonlyEntity;

        LiveObject _LiveEntity;
        readonly Func<TContext> _CreateNewContext;
        readonly Func<TContext, TEntity> _GetDbEntity;
        private bool _IsNew;



        private class LiveObject
        {
            public TContext DataContext { get; private set; }
            public TEntity Entity { get; private set; }

            public LiveObject(Func<TContext> createNewContext, Func<TContext, TEntity> _getDbEntity)
            {
                DataContext = createNewContext();
                Entity = _getDbEntity(DataContext);
            }
            public LiveObject(TContext dataContext, TEntity newEntity)
            {
                DataContext = dataContext;
                Entity = newEntity;
            }

        }

        public TEntity getEntity()
        {
            if (_LiveEntity != null)
                return _LiveEntity.Entity;
            else
                return _readonlyEntity;
        }

        #region Constructors


        ///// <summary>
        ///// Gets the entity, stores it using the function
        ///// for entities obtained from other relationships
        ///// </summary>
        ///// <param name="createNewContext">if a custom connection string is needed put it here</param>
        ///// <param name="getDbEntity">should use primary key to get back the same record again</param>
        //public BufferedLinqEntity2(Func<TContext> createNewContext, Func<TContext, TEntity> getDbEntity)
        //{
        //    _CreateNewContext = createNewContext;
        //    _GetDbEntity = getDbEntity;
        //    //readonlyContext.ObjectTrackingEnabled = false;
        //    _readonlyEntity = getDbEntity(createNewContext());

        //}


        /// <summary>
        /// Accepts an entity with whatever settings are preferred, for entities that were attached elsewhere
        /// </summary>
        /// <param name="createNewContext"></param>
        /// <param name="getDbEntity"></param>
        /// <param name="currentEntity"></param>
        public BufferedLinqEntity2(Func<TContext> createNewContext,
            Func<TContext, TEntity> getDbEntity, TEntity currentEntity)
        {
            _CreateNewContext = createNewContext;
            _GetDbEntity = getDbEntity;
            _readonlyEntity = currentEntity;
        }

        /// <summary>
        /// Used for new entities that are not attached to anything
        /// </summary>
        /// <param name="createNewContext"></param>
        /// <param name="getDbEntity"></param>
        /// <param name="newEntityCode"></param>
        /// <param name="InsertAction"></param>
        public BufferedLinqEntity2(Func<TContext> createNewContext, Func<TContext, TEntity, TEntity> getDbEntityFromCurrent
                , TEntity newEntityCode, Action<TContext, TEntity> InsertAction)
        {
            _CreateNewContext = createNewContext;
            _GetDbEntity = (dc) => getDbEntityFromCurrent(dc, _readonlyEntity);
            _readonlyEntity = newEntityCode;
            var newContext = createNewContext();
            InsertAction(newContext, newEntityCode);
            _LiveEntity = new BufferedLinqEntity2<TContext, TEntity>.LiveObject(newContext, newEntityCode);
        }

        #endregion

        #region Get/Set

        protected TFieldType GetBufferedProperty<TFieldType>(Func<TEntity, TFieldType> getValueFunc)
        {
            if (_LiveEntity != null)
                return getValueFunc(_LiveEntity.Entity);
            else
                return getValueFunc(_readonlyEntity);
        }


        protected void SetBufferedProperty(Action<TEntity> linqAction)
        {
            BeginEdit();
            //If something changes and context does exist, make change directly
            linqAction(_LiveEntity.Entity);
        }


        #endregion


        #region EditingMethods

        //if save is called try to commit changes
        //    if error report back or throw?
        //    if no error move _live entity to readonlyEntity

        public void BeginEdit()
        {
            //Does this need to be implemented at all?
            // when is this called when used with a bindingsource/binding navigator
            if (_LiveEntity == null)
            {
                //If something changes, and context does not exist, create context
                _LiveEntity = new LiveObject(_CreateNewContext, _GetDbEntity);
            }
        }

        public void CancelEdit()
        {
            _LiveEntity = null;
        }

        public void EndEdit()
        {
            var _newAndUnattached = IsNewAndUnattached;

            if (_LiveEntity != null)
            {
                OnEntitySaving(_readonlyEntity, _LiveEntity.Entity);
                _LiveEntity.DataContext.SubmitChanges();
                try
                {
                    OnEntitySaved(_readonlyEntity, _LiveEntity.Entity);
                }
                finally
                {
                    _readonlyEntity = _LiveEntity.Entity;
                    _LiveEntity = null;
                    if (_newAndUnattached) OnNewEntitySaved();
                }
                
            }
        }

        public bool IsNewAndUnattached
        {
            get
            {
                return _LiveEntity != null
                    && object.ReferenceEquals(_LiveEntity.Entity, _readonlyEntity);
            }
        }

        /// <summary>
        /// Performs the actual deleteOnSubmit and saveChanges
        /// </summary>
        /// <param name="dc"></param>
        /// <param name="entityToDelete"></param>
        protected abstract void DeleteEntity(TContext dc, TEntity entityToDelete);

        public void DeleteEntity()
        {
            if (IsNewAndUnattached)
            //entity is new and unattached
            {
                _LiveEntity = null;
                _readonlyEntity = null;
            }
            else
            {
                if (_LiveEntity != null)
                {
                    DeleteEntity(_LiveEntity.DataContext, _LiveEntity.Entity);
                }
                else if (_readonlyEntity != null)
                {
                    _LiveEntity = new BufferedLinqEntity2<TContext, TEntity>.LiveObject(_CreateNewContext, _GetDbEntity);
                    DeleteEntity(_LiveEntity.DataContext, _LiveEntity.Entity);
                }

            }

        }
        #endregion

        virtual protected void OnEntitySaving(TEntity oldEntity, TEntity newEntity) { }
        virtual protected void OnEntitySaved(TEntity oldEntity, TEntity newEntity) { }
        virtual protected void OnNewEntitySaved() { }

        public bool HasChanges
        {
            get { return _LiveEntity == null ? false : _LiveEntity.DataContext.HasChanges(); }
        }

        #region ConditionalMoves

        /// <summary>
        /// Code to attach to a bindingnavigator, and ask the user to save changes or not.
        /// </summary>
        /// <param name="form">used to make a call to Form.Validate
        /// in case the navigation is clicked while a control has not validated yet</param>
        /// <param name="bn">the bindingNavigator that is tied to your item</param>
        /// <param name="hasErrorsFunc">a call to a function that validates the current data showing, also called after a successful move
        /// ignored if null</param>
        /// <param name="checkErrors">usually clears all error provider errors,
        /// called just after moving,
        /// if the move is a success</param>
        /// <param name="OnMoveSuccess">What to do if the move is called usually something like bn.bindingsource.movenext, etc..</param>
        /// <param name="OnSaveRequest">usually a call to bn.current.directcast.endEdit()</param>
        public static DialogResult ConditionalMove(Form form, BindingNavigator bn
            , Func<bool> hasErrorsFunc,  Action OnMoveSuccess, Action OnSaveRequest)
        {
            Debug.Assert(bn.BindingSource != null);
            Debug.Assert(bn.BindingSource.Current != null);
            form.Validate();

            Action _OnMoveSuccess = () =>
            {
                
                OnMoveSuccess();
                hasErrorsFunc();
                //    itemChanges = false; 

            };

            var current = bn.BindingSource.Current.DirectCast<BufferedLinqEntity2<TContext, TEntity>>();

            bool _hasErrors = false;
            if (hasErrorsFunc != null)
                _hasErrors = hasErrorsFunc();
            else
                Debug.Print("hasErrorsFunc is null, ignoring");
            var _hasChanges = current != null ? current.HasChanges : false;
            String prompt;
            if (_hasErrors)
                prompt = "Cannot save because of errors,fix errors?";
            else
                prompt = "Save changes?";

            if (_hasErrors || _hasChanges) //|| itemChanges)

                switch (prompt.ToMessageBox("Unsaved changes"
                        , MessageBoxButtons.YesNoCancel))
                {
                    case DialogResult.Yes:
                        if (_hasErrors == false)
                        {
                            var saveSuccess = false;
                            try
                            {
                                OnSaveRequest();
                                saveSuccess = true;

                            }

                            catch (Exception exception)
                            {
                                exception.Message.ToMessageBox();
                                return DialogResult.Cancel;
                            }

                            if (saveSuccess) _OnMoveSuccess();
                            return DialogResult.Yes;
                        }
                        else
                            return DialogResult.Cancel;
                        

                    case DialogResult.No:
                        bn.BindingSource.CancelEdit();
                        current.CancelEdit();

                        _OnMoveSuccess();
                        return DialogResult.No;
                        

                    case DialogResult.Cancel:
                        return DialogResult.Cancel;
                    //does nothing user wishes to stay and work on the current record

                    default:
                        throw new NotImplementedException();
                        

                }

            else
            {
                _OnMoveSuccess();
                return DialogResult.None;
            }


        }

        #endregion
    }
}

2 comments:

  1. where i must implement this class? must i add a class file to my project? Because when i did it, the compiler showed errors at _LiveEntity.DataContext.HasChanges() and bn.BindingSource.Current.DirectCast. thank you for help me

    ReplyDelete
  2. Those 2 functions make use of extensions I have in that same library. check here https://code.msdn.microsoft.com/Release/ProjectReleases.aspx?ProjectName=ImaginaryDevelopment&ReleaseId=2881 for the entire project

    ReplyDelete