Como relatar e resolver conflitos de restrição
Este tópico descreve como usar uma linguagem gerenciada para permitir que um provedor personalizado padrão relate e resolva conflitos de restrição. Os conflitos de restrição são aqueles que violam as restrições impostas sobre itens, como a relação de pastas ou o local de dados que têm o mesmo nome em um sistema de arquivos. O Sync Framework oferece a classe NotifyingChangeApplier para ajudar a processar conflitos de restrição.
Para obter mais informações sobre conflitos de restrição, consulte Detectando e solucionando conflitos de restrição.
Este tópico pressupõe uma familiaridade básica com os conceitos de C# e do Microsoft .NET Framework.
Os exemplos deste tópico se concentram nas seguintes classes e métodos do Sync Framework:
A classe NotifyingChangeApplier.
O evento ItemConstraint da classe SyncCallbacks.
O método SaveChangeWithChangeUnits da interface INotifyingChangeApplierTarget.
O método RecordConstraintConflictForItem da classe SaveChangeWithChangeUnitsContext.
Noções básicas sobre conflitos de restrição
Os conflitos de restrição são detectados pelo provedor de destino durante a fase de aplicação de alterações da sincronização, normalmente nos métodos SaveItemChange ou SaveChangeWithChangeUnits. Quando o provedor de destino detecta um conflito de restrição, ele relata o conflito ao objeto NotifyingChangeApplier usando os métodos RecordConstraintConflictForChangeUnit ou RecordConstraintConflictForItem. O aplicador de alterações resolve o conflito de acordo com a política de resolução de conflitos de colisão definida para a sessão ou com a ação de resolução de conflito definida pelo aplicativo para o conflito especificado. O aplicador de alterações expede então as chamadas necessárias para o provedor de destino, como SaveItemChange ou SaveChangeWithChangeUnits, para que este possa aplicar o conflito resolvido à réplica de destino. Resoluções típicas de conflitos de restrição incluem renomear o item de origem ou o item de destino de forma que o conflito de restrição não ocorra mais ou mesclar o conteúdo dos dois itens em um único item.
Requisitos de compilação
.NET Framework 2.0 ou posterior.
Referência ao Microsoft.Synchronization.
Referência ao Microsoft.Synchronization.MetadataStorage.
Exemplo
O código de exemplo neste tópico mostra como habilitar o aplicador de alterações para tratar restrições de conflito, como detectar e relatar um conflito de restrição durante a fase de aplicação de alterações da sincronização, como definir a ação de resolução manipulando o evento ItemConstraint no aplicativo de sincronização e como resolver o conflito aplicando a resolução à réplica de destino. A réplica deste exemplo armazena contatos em um arquivo de texto como uma lista de valores separados por vírgula. Os itens a serem sincronizados são os contatos contidos nesse arquivo e as unidades de alteração rastreadas são os campos de cada contato. Os contatos são identificados de forma exclusiva no repositório de contatos por uma combinação dos campos name e phone number. Quando um contato com o mesmo name e o mesmo phone number é criado localmente em duas réplicas diferentes, ao sincronizar as réplicas ocorre um conflito de restrição de colisão.
Habilitando o objeto NotifyingChangeApplier para processar conflitos de restrição
Todos os requisitos a seguir devem ser atendidos para habilitar o objeto NotifyingChangeApplier para tratar conflitos de restrição com êxito.
Criar um objeto IConflictLogAccess
O aplicador de alterações usa um log de conflitos para salvar conflitos temporários durante a sincronização, de forma que os conflitos possam ser processados de forma eficiente durante a sessão. Para réplicas que de outra forma não armazenam conflitos, o Sync Framework oferece a classe MemoryConflictLog, que funciona na memória e pode ser usada para armazenar conflitos temporários durante a sessão de sincronização. Este exemplo cria o log de conflitos na memória ao iniciar a sessão.
public override void BeginSession(SyncProviderPosition position, SyncSessionContext syncSessionContext)
{
_sessionContext = syncSessionContext;
// Create an in-memory conflict log to store temporary conflicts during the session.
_memConflictLog = new MemoryConflictLog(IdFormats);
}
Passar o objeto IConflictLogAccess para o objeto NotifyingChangeApplier
O provedor de destino deve chamar o método ApplyChanges durante o processamento de seu método ProcessChangeBatch. Lembre-se de que o formulário de ApplyChanges chamado deve aceitar uma política de resolução de conflitos de colisão e um objeto IConflictLogAccess. Este exemplo chama ApplyChanges, especificando uma política de resolução de conflitos de colisão de ApplicationDefined e passando o log de conflitos na memória que foi criado em BeginSession.
public override void ProcessChangeBatch(ConflictResolutionPolicy resolutionPolicy, ChangeBatch sourceChanges, object changeDataRetriever, SyncCallbacks syncCallbacks, SyncSessionStatistics sessionStatistics)
{
// Use the metadata storage service to get the local versions of changes received from the source provider.
IEnumerable<ItemChange> localVersions = _ContactStore.ContactReplicaMetadata.GetLocalVersions(sourceChanges);
// Use a NotifyingChangeApplier object to process the changes. Specify a collision conflict resolution policy of
// ApplicationDefined and the conflict log that was created when the session started.
NotifyingChangeApplier changeApplier = new NotifyingChangeApplier(ContactStore.ContactIdFormatGroup);
changeApplier.ApplyChanges(resolutionPolicy, CollisionConflictResolutionPolicy.ApplicationDefined, sourceChanges,
(IChangeDataRetriever)changeDataRetriever, localVersions, _ContactStore.ContactReplicaMetadata.GetKnowledge(),
_ContactStore.ContactReplicaMetadata.GetForgottenKnowledge(), this, _memConflictLog, _sessionContext, syncCallbacks);
}
Implementar SaveConstraintConflict
O aplicador de alterações chama o método SaveConstraintConflict da interface INotifyingChangeApplierTarget2 para salvar conflitos temporários. Este exemplo adiciona a interface INotifyingChangeApplierTarget2 à classe que implementa KnowledgeSyncProvider.
class ContactsProviderWithConstraintConflicts : KnowledgeSyncProvider
, INotifyingChangeApplierTarget
, INotifyingChangeApplierTarget2
, IChangeDataRetriever
Este exemplo implementa SaveConstraintConflict usando o objeto MemoryConflictLog para salvar conflitos temporários e, caso contrário, lançando uma exceção.
public void SaveConstraintConflict(ItemChange conflictingChange, SyncId conflictingItemId,
ConstraintConflictReason reason, object conflictingChangeData, SyncKnowledge conflictingChangeKnowledge,
bool temporary)
{
if (!temporary)
{
// The in-memory conflict log is used, so if a non-temporary conflict is saved, it's
// an error.
throw new NotImplementedException("SaveConstraintConflict can only save temporary conflicts.");
}
else
{
// For temporary conflicts, just pass on the data and let the conflict log handle it.
_memConflictLog.SaveConstraintConflict(conflictingChange, conflictingItemId, reason,
conflictingChangeData, conflictingChangeKnowledge, temporary);
}
}
Implementar TryGetDestinationVersion
O aplicador de alterações deve poder obter a versão de um item de destino em conflito. Para isso, o provedor de destino implementa o método TryGetDestinationVersion da interface INotifyingChangeApplierTarget. Esse método é opcional quando não são relatados conflitos de restrição; caso contrário, é necessário.
public bool TryGetDestinationVersion(ItemChange sourceChange, out ItemChange destinationVersion)
{
bool found = false;
// Get the item metadata from the metadata store.
ItemMetadata itemMeta = _ContactStore.ContactReplicaMetadata.FindItemMetadataById(sourceChange.ItemId);
if (null != itemMeta)
{
// The item metadata exists, so translate the change unit metadata to the proper format and
// return the item change object.
ChangeUnitChange cuChange;
List<ChangeUnitChange> cuChanges = new List<ChangeUnitChange>();
foreach (ChangeUnitMetadata cuMeta in itemMeta.GetChangeUnitEnumerator())
{
cuChange = new ChangeUnitChange(IdFormats, cuMeta.ChangeUnitId, cuMeta.ChangeUnitVersion);
cuChanges.Add(cuChange);
}
destinationVersion = new ItemChange(IdFormats, _ContactStore.ContactReplicaMetadata.ReplicaId, sourceChange.ItemId,
ChangeKind.Update, itemMeta.CreationVersion, cuChanges);
found = true;
}
else
{
destinationVersion = null;
}
return found;
}
Relatando um conflito de restrição
Os conflitos de restrição são detectados pelo provedor de destino durante a fase de aplicação de alterações da sincronização. Este exemplo usa unidades de alteração, de forma que os conflitos de restrição são relatados no método SaveChangeWithChangeUnits chamando RecordConstraintConflictForItem quando uma ação de alteração de Create causar uma colisão de identidade no repositório de contatos.
case SaveChangeAction.Create:
{
// Create a new item. Report a constraint conflict if one occurs.
try
{
ConstraintConflictReason constraintReason;
SyncId conflictingItemId;
// Check if the item can be created or if it causes a constraint conflict.
if (_ContactStore.CanCreateContact(change, (string[])context.ChangeData, out constraintReason, out conflictingItemId))
{
// No conflict, so create the item.
_ContactStore.CreateContactFromSync(change, (string[])context.ChangeData);
}
else
{
// A constraint conflict occurred, so report this to the change applier.
context.RecordConstraintConflictForItem(conflictingItemId, constraintReason);
}
}
catch (Exception ex)
{
// Some other error occurred, so exclude this item for the rest of the session.
RecoverableErrorData errData = new RecoverableErrorData(ex);
context.RecordRecoverableErrorForItem(errData);
}
break;
}
O método CanCreateContact
do repositório de contatos que é chamado no exemplo acima usa um método DetectIndexCollision
particular para detectar se há uma colisão de identidade causada pelo contato a ser criado. Uma colisão de identidade ocorre quando o contato a ser criado contém os mesmos campos name e phone number que um contato existente.
public bool CanCreateContact(ItemChange itemChange, string[] changeData, out ConstraintConflictReason reason, out SyncId conflictingItemId)
{
bool canCreate = true;
// Create a temporary contact and see if its index values conflict with any item already in the contact store.
Contact newContact = new Contact(changeData);
canCreate = !DetectIndexCollision(newContact, out conflictingItemId);
if (!canCreate)
{
// An index collision occurred, so report a collision conflict.
reason = ConstraintConflictReason.Collision;
}
else
{
// This value won't be used because canCreate is set to true in this case.
reason = ConstraintConflictReason.Other;
}
return canCreate;
}
private bool DetectIndexCollision(Contact newContact, out SyncId conflictingItemId)
{
bool collision = false;
conflictingItemId = null;
// Enumerate the contacts in the list and determine whether any have the same name and phone number
// as the contact to create.
IEnumerator<KeyValuePair<SyncId, Contact>> contactEnum = _ContactList.GetEnumerator();
while (contactEnum.MoveNext())
{
if (contactEnum.Current.Value.IsIndexEqual(newContact))
{
// A conflicting item exists, so return its item ID and a value that indicates the collision.
conflictingItemId = contactEnum.Current.Key;
collision = true;
break;
}
}
return collision;
}
Definindo a ação de resolução para um conflito de restrição
Quando o provedor de destino especifica uma política de resolução de conflitos de colisão de ApplicationDefined, o aplicativo deve registrar um manipulador para o evento ItemConstraint antes de iniciar a sincronização.
// Register to receive the ItemConstraint event from both providers. Only the destination provider actually fires
// this event during synchronization.
((KnowledgeSyncProvider)localProvider).DestinationCallbacks.ItemConstraint += new EventHandler<ItemConstraintEventArgs>(HandleItemConstraint);
((KnowledgeSyncProvider)remoteProvider).DestinationCallbacks.ItemConstraint += new EventHandler<ItemConstraintEventArgs>(HandleItemConstraint);
O manipulador de eventos ItemConstraint determina como o conflito é resolvido. Este exemplo exibe os itens conflitantes para o usuário e pergunta como o conflito deve ser resolvido.
void HandleItemConstraint(Object sender, ItemConstraintEventArgs args)
{
if (ConstraintConflictReason.Collision == args.ConstraintConflictReason)
{
// Display the two items that are in conflict and solicit a resolution from the user.
Contact srcContact = new Contact((string[])args.SourceChangeData);
Contact destContact = new Contact((string[])args.DestinationChangeData);
string msg = "Source change is " + srcContact.ToString() +
"\nDestination change is " + destContact.ToString() +
"\nClick Yes to rename the source change and apply it." +
"\nClick No to rename the destination item and apply the source change." +
"\nClick Cancel to delete the destination item and apply the source change.";
ConstraintConflictDlg ccDlg = new ConstraintConflictDlg(msg);
ccDlg.ShowDialog();
// Set the resolution action based on the user's response.
args.SetResolutionAction(ccDlg.Resolution);
}
else
{
args.SetResolutionAction(ConstraintConflictResolutionAction.SaveConflict);
}
}
Tratando resoluções de conflito de restrição
Depois que a ação de resolução de conflito de restrição foi definida pelo aplicativo, o aplicador de alterações faz as alterações necessárias nos metadados associados com a resolução e expede uma chamada para o provedor de destino, de forma que ele possa aplicar a alteração à réplica de destino. Este exemplo usa unidades de alteração; então, o aplicador de destino chama o método SaveChangeWithChangeUnits do provedor de destino. As três resoluções possíveis que foram apresentadas ao usuário pelo aplicativo são tratadas neste método.
case SaveChangeAction.DeleteConflictingAndSaveSourceItem:
{
// Delete the destination item that is in conflict and save the source item.
// Make a new local version for the delete so it will propagate correctly throughout the synchronization community.
SyncVersion version = new SyncVersion(0, _ContactStore.ContactReplicaMetadata.GetNextTickCount());
_ContactStore.DeleteContactFromSync(context.ConflictingItemId, version);
// Save the source change as a new item.
_ContactStore.CreateContactFromSync(change, (string[])context.ChangeData);
break;
}
case SaveChangeAction.RenameDestinationAndUpdateVersionData:
{
// Rename the destination item so that it no longer conflicts with the source item and save the source item.
// Rename the destination item. This is done by appending a value to the name and updating the metadata to treat
// this as a local change, which will be propagated throughout the synchronization community.
_ContactStore.RenameExistingContact(context.ConflictingItemId);
// Save the source change as a new item.
_ContactStore.CreateContactFromSync(change, (string[])context.ChangeData);
break;
}
case SaveChangeAction.RenameSourceAndUpdateVersionAndData:
{
// Rename the source item so that it no longer conflicts with the destination item and save the source item.
// The destination item in conflict remains untouched.
// Rename the source item. This is done by appending a value to the name.
_ContactStore.FindNewNameForContact((string[])context.ChangeData);
// Save the renamed source change as a new item.
_ContactStore.CreateContactFromSync(change, (string[])context.ChangeData);
break;
}
Código de provedor completo
Para obter uma listagem completa do código de provedor usado neste documento, consulte Código de exemplo para relatar e resolver conflitos de restrição.
Próximas etapas
A seguir, talvez você queira implementar um log de conflitos que podem persistir além do final da sessão de sincronização. Para obter mais informações sobre a criação de um log de conflitos, consulte Registrando em log e gerenciando conflitos.
Consulte também
Conceitos
Programando tarefas comuns do provedor personalizado padrão
Detectando e solucionando conflitos de restrição