📜 ⬆️ ⬇️

Data synchronizer Developer note

At work and in everyday life, we often have to deal with various kinds of data synchronization . You synchronize files and folders of your phone with a computer, perform familiar actions in version control systems, use various kinds of Internet-based synchronization services for contacts, emails and documents, and often do not even think about how this process is implemented in one way or another.

If you decide to write your own synchronizer, you will most likely encounter a number of questions. In this article we will share the experience of writing such a component and consider the requirements for it. The basis of these requirements were all sorts of wishes we received from users, and real scenarios for using the XtraScheduler scheduler event synchronizer. Therefore, as examples of the code, we will provide code fragments from the specified product.

To begin with, we will determine which objects will participate in the synchronization process.
These are two data sets (source and target / final) and a synchronizer that performs a series of operations on these sets, as a result of which the target set must change in accordance with the implemented scenario.

Object synchronizer

The synchronization script will determine what functionality your synchronizer will have. If synchronization implies only one “main” copy and will be performed by complete replacement of the contents of the other, then a simple version of the type of importer / exporter will do. If you plan to synchronize sets of independent entries, you will have to make a more complex implementation of the synchronizer.
Base class

Create a base synchronizer class that defines the behavior common to all successors and the interface of methods, properties, and events. Let's call such a class, for example, SynchronizerBase. Such a class can define a strictly defined order of abstract method calls for performing the basic necessary synchronization actions in the desired sequence. The functionality will be expanded by inheritance. Having a base class will free you from code duplication. General operations such as initializing internal structures and properties can be implemented once in this class.

An additional advantage of this approach will be the fact that you get a single, harmonious API for all successors that are subsequently implemented.

public abstract class SynchronizerBase { //... protected SynchronizerBase(StorageBase storage) {...} public event ExchangeExceptionEventHandler OnException; protected internal abstract void SynchronizeCore(); public virtual void Terminate() {...} public virtual void Synchronize() { Storage.BeginUpdate(); try { ResetTermination(); SynchronizeCore(); } finally { Storage.EndUpdate(); } } } 

Segregation of duties

Depending on the scenarios of working with data sets, you can implement the corresponding heirs of SynchronizerBase, who “can” perform the sequence of actions laid down in the scenario, knowing in advance which set is the “main” one. Such specialized classes will be much easier to use than setting up and using the only one, but "who can do everything at once."

Thus, you can create several inheritors, for example, ImportSynchronizer and ExportSynchronizer, which implement the basic synchronization logic for each of the scenarios. These classes can remain abstract if in the future you plan to implement their concrete heirs for different data sets.

For example, in XtraScheduler we have the following base class scheme:

Allocation of algorithms to subclasses

In order not to make the synchronizer object too heavy, it makes sense to organize the architecture by separating the implementation of the synchronization algorithm from the synchronizer interface itself. Select a subclass in the class synchronizer, while not forgetting to establish interaction between these objects.

 public class ImportSynchronizeManager : ImportManager { public ImportSynchronizeManager(AppointmentExchanger synchronizer) : base(synchronizer) { } protected internal override void PerformExchange() { Appointments.BeginUpdate(); try { PrepareSynchronization(); SynchronizeAppointments(); DeleteNonSynchronizedAppointments(); } finally { Appointments.EndUpdate(); } } } public class ExportSynchronizeManager : ExportManager { public ExportSynchronizeManager(AppointmentExchanger synchronizer) : base(synchronizer) { } protected internal override void PerformExchange() { PrepareSynchronization(); SynchronizeOutlookAppointments(); CreateNewOutlookAppointments(); } } 

Operations on dataset objects

Depending on the synchronization scenario, data sets can be modified in different ways. But, as a rule, all actions with objects of sets are reduced to three main actions:

All operations described above can be reduced to the following table:

The described actions should be implemented in one form or another in each of the heirs of your synchronizer. At the same time, it is necessary to take into account an important point, which copy of the data is considered to be “main”. Depending on the choice of source and target sets may be interchanged. That is why the ImportSynchronizer and ExportSynchronizer classes will perform the opposite actions on data sets.

Event support

Undoubtedly, when performing any action on set objects, the user will want to be able to get access to these objects “before” and “after” the operation. Arrange a pair of Synchronizing and Synchronized events in the base class.

Define the SynchronizingEventArgs and SynchronizedEventArgs event handler arguments and add the fields and properties for the corresponding objects from the synchronized sets. In the case when in the base class it cannot be done immediately, use the inheritance of the arguments and make the missing properties in the heirs.

 public class AppointmentEventArgs : EventArgs { public AppointmentEventArgs(Appointment apt) { } public Appointment Appointment { get { ... } } } public class AppointmentCancelEventArgs : AppointmentEventArgs { public AppointmentCancelEventArgs(Appointment apt) : base(apt) { } public bool Cancel { get { ... } } } public class AppointmentSynchronizingEventArgs : AppointmentCancelEventArgs { public AppointmentSynchronizingEventArgs(Appointment apt) : base(apt) { } public SynchronizeOperation Operation { get { ... } } } public class OutlookAppointmentSynchronizingEventArgs : AppointmentSynchronizingEventArgs { public OutlookAppointmentSynchronizingEventArgs(Appointment apt, _AppointmentItem olApt) : base(apt) { } public _AppointmentItem OutlookAppointment { get { ... } } } 

Consider the need for an exception handling event. For example, you can implement the OnException event and redirect the exceptions received there. Let the user decide whether to continue the synchronization process after an exception occurs. Complete the event arguments with all the necessary information.

 iCalendarImporter importer; //... importer.OnException += new ExchangeExceptionEventHandler(importer_OnException); void importer_OnException(object sender, ExchangeExceptionEventArgs e) { iCalendarParseExceptionEventArgs args = e as iCalendarParseExceptionEventArgs; if (args != null) { ShowErrorMessage(String.Format("Cannot parse line {0} with text '{1}'", args.LineIndex, args.LineText)); } else ShowErrorMessage(e.OriginalException.Message); e.Handled = true; // prevent this exception from throwing } } 

Data integrity

Support for the performance of each operation on the objects set the ability to cancel. This can be done by adding the Cancel property to the arguments of the SynchronizingEventArgs event.

Be sure that this will be useful for executing the “merging” script of two independent data sets, when it makes sense to cancel all deletion operations when synchronizing first, first one way and then the other.

 AppointmentImportSynchronizer synchronizer; //... synchronizer.AppointmentSynchronizing += new AppointmentSynchronizingEventHandler(synchronizer_AppointmentSynchronizing); void synchronizer_AppointmentSynchronizing(object sender, AppointmentSynchronizingEventArgs e) { // ... if (ShouldCancelOperation) e.Cancel = true; } 

Cover all object manipulation scenarios

Provide a situation not only to cancel the proposed "default" action on the object, but also to give the opportunity to perform another possible operation.

For example, add the additional property SynchronizeOperation {Create, Replace, Delete} to the arguments and let the user specify the desired value. This way, you will be able to delete the object on the target data set instead of replacing it with a copy from the source set.

Such an approach makes it possible to more accurately handle “edit conflicts” in data sets.

 void synchronizer_AppointmentSynchronizing(object sender, AppointmentSynchronizingEventArgs e) { switch (e.Operation) { case SynchronizeOperation.Replace: if (ShouldDeleteInsteadReplace.Checked) e.Operation = SynchronizeOperation.Delete; break; } 

UI Elements

Try to design a synchronizer so that it does not use the issuance of messages, dialogs and other visual elements. Because there is always a user who needs to use your component, performing synchronization in a stream created from an application or, for example, from a special service.

Data caching

Sometimes it makes sense to load a dataset, saving its objects in the synchronizer. This makes sense when it is desirable to avoid repeated calls to a real data set. In addition, during synchronization, the target data set may change, which can potentially manifest itself in problems associated with iteration over this set from the synchronizer code.

Parameterization of the data set

Sooner or later, the user will want to synchronize not the entire set, but only a part of it, limited, for example, by a time interval or a specific parameter.

The solution will be to write the source data provider with the ability for the user to “slip” his own provider, which determines his logic. In this case, the synchronizer should receive data not directly, but through the provider. In this case, you will have to use the above data caching inside the synchronizer.

 void DoFilteredImport() { OutlookImport importer = new OutlookImport(this.schedulerStorage1); TimeInterval interval = CalculateCurrentWeekInterval(); ParameterizedCalendarProvider provider = new ParameterizedCalendarProvider(interval); importer.SetCalendarProvider(provider); importer.Import(System.IO.Stream.Null); } 

Process completion

Provide the ability to complete the synchronization process as desired by the user. For example, get the Terminate method in the SynchronizerBase base class. Such a function can be useful when an exception has occurred during synchronization of large data sets or the collection object does not satisfy certain conditions and it is necessary to immediately stop further execution. In this case, the user will not have to wait until the end of the process.

 void synchronizer_OnException(object sender, ExchangeExceptionEventArgs e) { // ... AppointmentImportSynchronizer synchronizer = (AppointmentImportSynchronizer)sender; synchronizer.Terminate(); e.Handled = true; } 


It may happen that the user wants to synchronize properties other than those that your component synchronizes. Provide the ability to define this at the level of setting custom properties or objects and provide all the necessary API for this.

 void exporter_AppointmentExporting(object sender, AppointmentExportingEventArgs e) { iCalendarAppointmentExportingEventArgs args = e as iCalendarAppointmentExportingEventArgs; AddEventAttendees(args.VEvent, “test@corp.com”); } private void AddEventAttendee(VEvent ev, string address) { TextProperty p = new TextProperty("ATTENDEE", String.Format("mailto:{0}", address)); p.AddParameter("CN", address); p.AddParameter("RSVP", "TRUE"); ev.CustomProperties.Add(p); } 

Allow users to inherit from your synchronizer to override any functionality. Provide this when designing classes and methods.

 public class MyOutlookImportSynchronizer : OutlookImportSynchronizer { public MyOutlookImportSynchronizer(SchedulerStorageBase storage) : base(storage) { } protected override OutlookExchangeManager CreateExchangeManager() { return new MyExchangeManager(); } } 

Helper classes

Create methods for getting synchronizer initialization parameters (for example, a directory pointing to a specific calendar) or any other information necessary for synchronization. For more convenient use, place them in separate auxiliary classes or make them available directly in the component.

 public class OutlookExchangeHelper { public static string[] GetOutlookCalendarNames(); public static string[] GetOutlookCalendarPaths() ; public static List<OutlookCalendarFolder> GetOutlookCalendarFolders(); } public class OutlookUtils { public static _Application GetOutlookApplication(); public static void ReleaseOutlookObject(object obj); } public class iCalendarConvert { public static TimeSpan ToUtcOffset(string value); public static string FromUtcOffset(TimeSpan value) ; public static string UnescapeString(string str); } 

We really hope that the above “collection of tips” will help you best determine the amount of functionality you need before writing a synchronizer and provide an opportunity to avoid mistakes in the process of its implementation.

Source: https://habr.com/ru/post/106063/

All Articles