locked
Conflict Handling Across WCF RRS feed

  • Question

  • I am implementing sync services across WCF, which so far seems to be working well. However, I need to be able to allow the client to resolve conflicts. From what I can see, you would want to subscribe to the ApplyChangeFailed event. That would be fine, but it only exists under the ServerSyncProvider which, unless I am missing something, cannot be accessed client side. I know you can subscribe to this event on the server, from within the WCF application in my case. But handling this server side only limits you as the user would not be able to decide how the conflict should be handled.

     

    My question is what is the recommended way of handling conflicts client side in an n-tier environment and is it possible to handle the ApplyChangeFailed event client side?

    • Moved by Hengzhe Li Friday, April 22, 2011 3:09 AM (From:SyncFx - Microsoft Sync Framework Database Providers [ReadOnly])
    Tuesday, October 2, 2007 9:08 AM

All replies

  • I have the same question.

    Sync. Services looks great on paper but in practice there are a lot of holes in its current implementation.  I'd love to be able to use it a new project I'm developing but there's only so long I can wait before I'll have to roll my own solution.

    Anybody from the sync. services team have any suggestions?  Thanks.
    Thursday, October 25, 2007 7:07 PM
  • Hi All,

     

    The Client Provider also fires the same event. You can subscribe to the event on the client and address any conflict as they are detected.

     

    Where to resolving conflicts is largely dependent on your scenario. Sync Services collects all the unresolved conflicts on the server and client and pass them back to the client at the end of the sync. This way, the application on the client then shows the use the conflicts and request a resolution, then resync again.

     

    Thanks,

    Rafik

     

     

     

    Thursday, October 25, 2007 7:35 PM
  • Rafik,

    Thanks for the quick response.

    Are you saying we should listen to the client provider's ApplyChangeFailed event for notification of the server provider's failure to apply changes?  That seems counter intuitive to me.  I would expect the client's ApplyChangeFailed event to fire when changes from the server fail to get applied to the client, not the other way around.  Could you clarify or post a small example snippet?

    Thanks,

    Zac
    Friday, October 26, 2007 4:19 PM
  • That's not exactly what I meant. Let me clarify.

    Conflicts can occur on client or on server. The ApplyChangeFailed event is designed to give you a chance to resolve the conflict as sync progresses. If you decided to ignore the conflict, then the info on the conflict will be stored till the end of the sync such that you can execute any post-mortem processing on the client side.

     

    With that, your intuition is correct. As for conflicts that occur on the server you can process them at the end of sync.

     

    Now, if you feel you want the client to resolve server conflicts at the time they are detected, you could extend the runtime and pass the conflict to your client. The drawback of this offcouse is that you will hold the transaction longer till the client decides how to resolve the conflict.

     

    Thanks

    Friday, October 26, 2007 5:01 PM
  • Rafik,

     

    Thank you for your input so far. My original question still stands. You mentioned:

     

    Now, if you feel you want the client to resolve server conflicts at the time they are detected, you could extend the runtime and pass the conflict to your client.

     

    That is what my original question was... how can we extend the runtime to relay the conflicts back to the client and how do we then push those changes back to the server?
    Wednesday, October 31, 2007 6:05 AM
  • Hi,

     

    I have the same problem. The only conflict type I also get on the client is ClientInsertServerInsert. In this case I get the event on server and client. In all other cases I only got the event on the server. Since there is no user at the server, I have no poosibility to decided which version to take. An additional problem is, that Continue doesn't behave like documented.

     

    Documentation: Continue processing, and add the row to the list of conflicts that are defined in SyncConflict. This is the default behavior.

     

    So what I expected is, that Continue doesn't decide which version to take and simply continues. In the example I testesd (Demo IV) I used the Update-Update Conflict. I got the event on the Server and after selecting Continue, the client version doesn't overwrite the server version but afterwards the server overwrites the client without asking me again. So I didn't have the possiblity to decide post-mortem, since the client version was already overwritten. I had the same behaviour in update-delete and in delete-update, so everywhere except in insert-insert. Since update-update is the most common type of conflict, this is a big problem for me.

     

    By the way: In Insert-Insert Continue behaves a little bit other. First I get the conflict event on the server. If I click on Continue nothing happens. Afterwards the client gets the event and again nothing happens, when clicking on Continue. But now there is another problem. Client and Server have their own version and even if I click on synchonize later on, they will always keep their version and there is no way to merge.

     

    How can I resolve a conflict post-mortem on a client?

     

     

    Friday, November 2, 2007 9:25 AM
  • Hi,

     

    I think I found a possiblity to resolve a conflict post-mortem. Therefore I adapted Demo IV. What happens: I start the synchronization for the first time and collect all conflicts in a list. Before applying changes I throw an exception with the conflictlist and rollback all applied changes. Now the user has time to solve his conflicts and decides which versions to take. Afterwards he starts the synchronization again. The server has a list with the decisions and automatically resolves the conflicts now.

    If the server version has changed meanwhile, it will be recognized again, so conflict on conflict is supported. If a new conflict occurs, than the synchonisation will again return the conflicts to the client until the server can solve all conflicts automatically.

     

    Now to the code. In SyncForm I changed region 7. ApplyChangeFailed calls now two different methods, depending on client or server. _confExc is a global variable of custom type ConflictException. Additionally the server is registered for ChangesApplied.

    Code Block

    clientSyncProvider.SyncProgress += new EventHandler(ShowProgress);
    clientSyncProvider.ApplyChangeFailed += new EventHandler(ShowClientFailures);

     

    _confExc = null;
    serverSyncProvider.SyncProgress += new EventHandler(ShowProgress);
    serverSyncProvider.ApplyChangeFailed += new EventHandler(ShowServerFailures);
    serverSyncProvider.ChangesApplied += new EventHandler(serverSyncProvider_ChangesApplied);
                    SyncStatistics syncStats = _syncAgent.Synchronize();

     

    ......

                }
                catch (ConflictException conf)
                {
                    ConflictExceptionResolution(conf);
                }

     

     

     

    My own Exception:

    Code Block

        public class ConflictException : Exception
        {
            private List _args;

            public ConflictException()
            {
                _args = new List();
            }

            public List ApplyChangeFailedEventArgsList
            {
                get { return _args; }
                set { _args = value; }
            }

            public void AddApplyChangeFailedEventArgs(ApplyChangeFailedEventArgs item)
            {
                _args.Add(item);
            }
        }

     

     

     

    ShowClientFailures is not changed but ShowServerFailures. When calling Synchronization the first time, _list will always be null, so every conflict will be stored in the ConflictException. When calling Synchronization for the second time, _list won't be null and the server can compare and resolve all conflicts. Therefore he will take the decision from the user (item.Action) and set it for the actual conflict.

    Code Block
            public void ShowServerFailures(object syncAgent, ApplyChangeFailedEventArgs args)
            {
                if (_list != null && _list.Count > 0)
                {
                    foreach (ApplyChangeFailedEventArgs item in _list)
                    {
                        if (CompareConflicts(args.Conflict, item.Conflict))
                        {
                            args.Action = item.Action;
                            return;
                        }
                    }
                }
                if (_confExc == null)
                {
                    _confExc = new ConflictException();
                }
                _confExc.AddApplyChangeFailedEventArgs(args);

            }

     

     

     

    CompareConflicts is quite simple and compares all cells:

           

    Code Block

    private bool CompareConflicts(SyncConflict actualConflict, SyncConflict savedConflict)
            {
                //ServerChange 
                DataTable actTable = actualConflict.ServerChange;
                DataTable savedTable = savedConflict.ServerChange;
                for (int i = 0; i < actTable.Columns.Count; i++)
                {
                    if (!actTable.Rows[0][i].Equals(savedTable.Rows[0][i]))
                    {
                        return false;
                    }
                }

                //ClientChange 
                actTable = actualConflict.ClientChange;
                savedTable = savedConflict.ClientChange;
                for (int i = 0; i < actTable.Columns.Count; i++)
                {
                    if (!actTable.Rows[0][i].Equals(savedTable.Rows[0][i]))
                    {
                        return false;
                    }
                }

                return true;
            }

     

     

    After all conflicts have been collected, the Server will reach the event ChangesApplied. If there have been any conflicts, the ConflictException will be thrown.

          

    Code Block
      private void serverSyncProvider_ChangesApplied(object sender, ChangesAppliedEventArgs e)
            {
                if (_confExc != null)
                {
                    throw _confExc;
                }
            }

     

     

    The client now gets the Exception and calls a CustomForm for each Conflict, where the User can decide between Client- and Serverversion

          

    Code Block
      private void ConflictExceptionResolution(ConflictException conf)
            {
                //Behebe Konflikt
                foreach (ApplyChangeFailedEventArgs item in conf.ApplyChangeFailedEventArgsList)
                {
                    CustomConflictForm form = new CustomConflictForm();
                    form.HandleConflict(item);
                    form.ShowDialog();
                }
                if (_list == null)
                {
                    _list = new List();
                }
                foreach (ApplyChangeFailedEventArgs args in conf.ApplyChangeFailedEventArgsList)
                {
                    _list.Add(args);
                }
            }

     

     

     

    Custom Form:

    Code Block

    public partial class CustomConflictForm : Form
        {
            ApplyChangeFailedEventArgs _conflictArgs;


            public CustomConflictForm()
            {
                InitializeComponent();
            }

            public void HandleConflict(ApplyChangeFailedEventArgs args)
            {                       
                _conflictArgs = args;

                this.Text = "Custom Conflict Resolution";    
                textBoxSyncStage.Text = _conflictArgs.Conflict.SyncStage.ToString();
                textBoxConflictType.Text = _conflictArgs.Conflict.ConflictType.ToString();                       
                textBoxError.Text = _conflictArgs.Conflict.ErrorMessage;

                if (_conflictArgs.Conflict.ServerChange != null)
                {
                    dataGridServerChange.DataSource = _conflictArgs.Conflict.ServerChange;
                }

                if (_conflictArgs.Conflict.ClientChange != null)
                {
                    dataGridClientChange.DataSource = _conflictArgs.Conflict.ClientChange;
                }

                Application.DoEvents();
            }


            private void btnOk_Click(object sender, EventArgs e)
            {
                if (rbServer.Checked)
                {
                    _conflictArgs.Action = ApplyAction.Continue;
                }
                else
                {
                    _conflictArgs.Action = ApplyAction.RetryWithForceWrite;
                }
                Close();
            }
        }

     

     

    The controls of the custom form:

    Code Block
            private System.Windows.Forms.TextBox textBoxSyncStage;
            private System.Windows.Forms.TextBox textBoxConflictType;
            private System.Windows.Forms.TextBox textBoxError;
            private System.Windows.Forms.DataGridView dataGridServerChange;
            private System.Windows.Forms.DataGridView dataGridClientChange;
            private System.Windows.Forms.Button btnOk;
            private System.Windows.Forms.RadioButton rbServer;
            private System.Windows.Forms.RadioButton rbClient;

     

     

     

    This is my first prototyp for post-mortem conflict resolution. There are still a few problems, for e.g. how will the server get the list of conflicts, if it is a webservice and where to store it. There is also an exception which already existed in Demo IV: When the user decides to resolve one conflict with forceWrite, all conflicts will be resolved this way.

     

    Is there a better and more efficient way to resolve a conflict post-mortem? Maybe someone has a solution where synchronization has to be started only once?

    Monday, November 12, 2007 1:14 PM
  • You may try not using  Bidirectional Synchronization directly.
    Sync with download only to local copy and store all the conflict record in a conflict table.


    let the user resolve the conflict in locally.
    with the conflict table user can compare server and local version in local database.

    After user update conflict record locally.
    user may perform synchronization again with the server.
    Thursday, December 18, 2008 1:22 PM
  • Question:

    I have a wcf  upload only Sync. I am forcing an error by sending a duplicate primary key  to the server.

    After Sync , SyncStatistics shows UploadChangesFailed =1. 

    But the ApplyChangeFailed event does not fire, even though I added a event handler like this :

    clientSyncProvider.ApplyChangeFailed += new EventHandler<ApplyChangeFailedEventArgs>(clientSyncProvider_ApplyChangeFailed);

    Previously , I thought Rafik said that the Server error is sent to the client provider. So why don't I see it?

    Thanks,
    Peter

    Wednesday, June 3, 2009 9:04 PM