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
    }
}

Thursday, September 24, 2009

Error management on a windows forms application form

I've been working on this as to help keep my error status straight on my windows form


//begin EPM
        private class ErrorProviderManager
        {
            private ErrorProvider _ep;
            Dictionary<Control, List<KeyValuePair<Func<bool>, String>>> dic =
                new Dictionary<Control, List<KeyValuePair<Func<bool>, String>>>();
            Dictionary<Control, List<String>> dicPossibleErrorStrings = new Dictionary<Control, List<String>>();
            List<Control> verifyWithErrorProviderList = new List<Control>();

            public ErrorProviderManager(ErrorProvider ep)
            {
                _ep = ep;
            }
            public bool hasErrors()
            {
                var result = false;
                foreach (var item in dic.Keys)
                {

                    var itemHasErrors = hasErrors(item);
                    if (itemHasErrors) result = true;
                }
                foreach (var item in verifyWithErrorProviderList)
                {
                    if (_ep.GetError(item).IsNullOrEmpty() == false)
                        result = true;
                }
                return result;
            }

            public bool hasErrors(Control item)
            {


                var itemHasErrors = false;
                if (_ep.GetError(item).IsNullOrEmpty() == false && dicPossibleErrorStrings[item].Contains(_ep.GetError(item)) == false)
                    itemHasErrors = true;
                else
                {
                    foreach (var kvp in dic[item])
                    {
                        if (itemHasErrors == false && kvp.Key.Invoke())
                        {
                            _ep.SetError(item, kvp.Value);
                            itemHasErrors = true;
                            break;
                        }

                    }

                    if (itemHasErrors == false)
                        _ep.SetError(item, string.Empty);
                }
                return itemHasErrors;
            }

            public void AddErrorCondition(Control c, Func<bool> errorCondition, String errorMessage)
            {
                if (errorMessage.IsNullOrWhitespace() == false)
                {
                    if (verifyWithErrorProviderList.Contains(c))
                        verifyWithErrorProviderList.Remove(c);
                    if (dic.ContainsKey(c) == false)
                        dic.Add(c, new List<KeyValuePair<Func<bool>, string>>());
                    dic[c].Add(new KeyValuePair<Func<bool>, String>(errorCondition, errorMessage));
                    if (dicPossibleErrorStrings.ContainsKey(c) == false)
                        dicPossibleErrorStrings.Add(c, new List<string>());
                    dicPossibleErrorStrings[c].Add(errorMessage);
                }
            }
            public void AddCheckForCustomErrorProviderText(Control c)
            {
                if (dicPossibleErrorStrings.ContainsKey(c) == false && verifyWithErrorProviderList.Contains(c) == false)
                    verifyWithErrorProviderList.Add(c);
            }

            private void _ClearError(Control c)
            {
                _ep.SetError(c, string.Empty);
            }

            public void refreshErrors()
            {
                hasErrors();
            }

        }
        //end EPM

Thursday, September 17, 2009

DRY Documentation

Excellent information from "The Productive Programmer":


Note: Out-of-date documentation is worse than none because it is actively misleading.

Documentation is a classic battleground between management and developers: managers want more and developers want to create less. It is also a battleground in the war against noncanonical representations. Developers should be able to make changes to code aggressively, to improve its structure and allow it to evolve. If you must have documentation for all your code, it must evolve at the same time. But most of the time, they get out of sync because of schedule pressure, lack of motivation (because , let's face it, writing code is more fun than writing documentation), and other factors.
Note: for managers documentation is about risk mitigation.

Out-of-date documentation  creates the risk of spreading misinformation (which is ironic, given that part of its purpose is to reduce risk).
  - Neal Ford

Tuesday, September 15, 2009

buffering linq changes for row at a time submits

So it appears linq-to-sql objects do not support anything to cancel changes for an individual row. Here's the parent class I'm using on my business object wrapper between the ui, and my linq entities:
public class BufferedLinqChange:System.ComponentModel.IEditableObject
{
LqGpsDataContext _dataContext;
 
internal BufferedLinqChange(LqGpsDataContext dataContext)
{
_dataContext = dataContext;
}
 
protected void SetBufferedProperty<T>(string key,Action linqAction
,bool linqEqualsValue,Action bufferAction)
{
if (linqEqualsValue)
{
if (Changes.ContainsKey(key))
Changes.Remove(key);
}
else
Changes.InsertOrUpdate(key, linqAction); bufferAction();
}
 
protected Dictionary<String, Action> Changes = new Dictionary<string, Action>();
 
public int ChangeCount { get { return Changes != null ? Changes.Count : 0; } }
public bool hasChanges { get { return Changes != null ? Changes.Count > 0 : false; } }
 
/// <summary>
/// Learned about this from http://www.vbforums.com/showthread.php?t=584096
/// Other sources:
/// Is IEditableObject too hard?
/// http://go.internet.com/?id=474X1146&url=http%3A%2F%2Fwww.madprops.org%2Fblog%2Fis-ieditableobject-too-hard%2F
/// </summary>
#region IEditableObject Members
 
public void BeginEdit()
{
}
 
public void CancelEdit()
{
if (Changes != null)
Changes.Clear();
}
 
public void EndEdit()
{
_dataContext.SubmitChanges();
if (ChangeCount > 0)
{
Changes.ForEach((item) => item.Value.Invoke());
_dataContext.SubmitChanges();
}
}
 
#endregion
}
A sample property in snippet form would be implemented like this:

#region $propertyName$
 
 
$fieldType$_$propertyName$;
public const String STR_$propertyName$ = "$propertyName$";
public $fieldType$ $propertyName$
{
get { return (Changes.ContainsKey(STR_$propertyName$) ? _$propertyName$ : $entityHolder$.$propertyName$); }
set
{
SetBufferedProperty<$fieldType$>(STR_$propertyName$
, () => $entityHolder$.$propertyName$ = value, $entityHolder$.$propertyName$ == value, () => _$propertyName$ = value);
}
}
#endregion

I'll probably go back and make bufferedLinqChange generic so it can hold and require the linq entity type to be defined. But you'll find since the business object now supports IEditableObject, moving to a different record when it is in a bindingSource, automatically starts the beginEdit, and calls endEdit. I overrode that behavior so that a prompt is given to the user to decide to save or not before navigating. The before navigation code is as follows:

private void ConditionalMove(Action OnMoveSuccess)
{
this.Validate();
Action _OnMoveSuccess = () => { OnMoveSuccess();
//    itemChanges = false; 
};
var current = this.bnAssets.BindingSource.Current.DirectCast<ModelAsset>();
if (_epm.hasErrors() || current.hasChanges) //|| itemChanges)
switch ("Unsaved changes exist save?".ToMessageBox("Save changes", MessageBoxButtons.YesNoCancel))
{
case DialogResult.Yes:
this.bnAssets.BindingSource.EndEdit();
var saveSuccess = false;
try
{
this.dc.dc.SubmitChanges();
saveSuccess = true;
}
catch (Exception exception)
{
exception.Message.ToMessageBox();
}
if (saveSuccess) _OnMoveSuccess();
break;
case DialogResult.No:
current.CancelEdit();
this.errorProvider.Clear();
_OnMoveSuccess();
break;
case DialogResult.Cancel:
 
default:
break;
}
else
_OnMoveSuccess();
}

Then a navigation button would point to:

private void bindingNavigatorMoveNextItem_Click(object sender, EventArgs e)
{
ConditionalMove(() => this.bnAssets.BindingSource.MoveNext());
}

Monday, September 7, 2009

DataTimePicker databinding vs nullable DateTime

I just came across an interesting problem. You can't bind a nullable dateTime directly to a dateTimePicker. At least not easily. Here's an extension method that handles the heavy lifting.


/// <summary>
/// 
/// </summary>
/// <param name="dtp"></param>
/// <param name="dataSource"></param>
/// <param name="valueMember"></param>
/// <remarks>With help from Dan Hanan at http://blogs.interknowlogy.com/danhanan/archive/2007/01/21/10847.aspx</remarks>
public static void BindNullableValue(this DateTimePicker dateTimePicker, BindingSource dataSource, String valueMember)
{
var binding = new Binding("Value", dataSource, valueMember, true);
//OBJECT PROPERTY --> CONTROL VALUE
binding.Format += new ConvertEventHandler((sender, e) =>
{
Binding b = sender as Binding;
if (b != null)
{
DateTimePicker dtp = (binding.Control as DateTimePicker);
if (dtp != null)
{
if (e.Value == null)
{
dtp.ShowCheckBox = true;
dtp.Checked = false;
 
// have to set e.Value to SOMETHING, since it's coming in as NULL
// if i set to DateTime.Today, and that's DIFFERENT than the control's current
// value, then it triggers a CHANGE to the value, which CHECKS the box (not ok)
// the trick - set e.Value to whatever value the control currently has. 
// This does NOT cause a CHANGE, and the checkbox stays OFF.
 
e.Value = dtp.Value;
 
}
else
{
dtp.ShowCheckBox = true;
dtp.Checked = true;
// leave e.Value unchanged - it's not null, so the DTP is fine with it.
}
 
}
 
}
});
// CONTROL VALUE --> OBJECT PROPERTY
binding.Parse += new ConvertEventHandler((sender, e) => {
// e.value is the formatted value coming from the control. 
// we change it to be the value we want to stuff in the object.
Binding b = sender as Binding;
 
if (b != null)
{
DateTimePicker dtp = (b.Control as DateTimePicker);
if (dtp != null)
{
if (dtp.Checked == false)
{
dtp.ShowCheckBox = true;
dtp.Checked = false;
e.Value = (Nullable<DateTime>) null;
}
else
{
DateTime val = Convert.ToDateTime(e.Value);
e.Value =val;
}
}
}
});
dateTimePicker.DataBindings.Add(binding);
 
}