如何报告和解决约束冲突

本主题说明如何使用托管语言来支持标准自定义提供程序报告和解决约束冲突。约束冲突指违反有关项的约束(如文件夹的关系或文件系统中同名数据的位置)的冲突。Sync Framework 提供 NotifyingChangeApplier 类来帮助处理约束冲突。

有关约束冲突的更多信息,请参见检测和解决约束冲突

本主题假定您基本熟悉 C# 和 Microsoft .NET Framework 概念。

本主题中的示例着重介绍以下 Sync Framework 类和方法:

了解约束冲突

约束冲突是在同步的变更应用阶段由目标提供程序检测到的冲突,一般通过 SaveItemChangeSaveChangeWithChangeUnits 方法检测。目标提供程序检测到约束冲突时,它使用 RecordConstraintConflictForChangeUnitRecordConstraintConflictForItem 方法将该冲突报告给 NotifyingChangeApplier 对象。变更应用方根据为会话设置的抵触冲突解决策略或应用程序为指定冲突设置的冲突解决操作来解决冲突。然后,变更应用方对目标提供程序调度任何必要的调用(如 SaveItemChangeSaveChangeWithChangeUnits),以便目标提供程序可以将已解决的冲突应用到目标副本。约束冲突的典型解决方法包括:重命名源项或目标项以便不再发生约束冲突,或将两个项的内容合并为单个项。

生成要求

示例

本主题中的示例代码说明如何使变更应用方可以处理约束冲突、如何在同步的变更应用阶段检测和报告约束冲突、如何在同步应用程序中通过处理 ItemConstraint 事件设置解决操作以及如何通过将解决方法应用到目标副本来解决冲突。本示例中的副本将联系人以逗号分隔的值列表形式存储在文本文件中。要同步的项为包含在此文件中的联系人,跟踪的变更单位是每个联系人中的字段。联系人在联系人存储区中由 name 和 phone number 字段的组合唯一标识。如果在两个不同副本中在本地创建具有相同 name 和 phone number 的联系人,则同步这两个副本时会发生抵触约束冲突。

允许 NotifyingChangeApplier 对象处理约束冲突

必须满足以下所有要求,NotifyingChangeApplier 对象才能成功处理约束冲突。

创建 IConflictLogAccess 对象

变更应用方在同步期间使用冲突日志来保存临时冲突,以便在会话期间高效地处理冲突。对于不存储冲突的副本,Sync Framework 提供了 MemoryConflictLog 类,该类在内存中运行并可用于在同步会话期间存储临时冲突。本示例在会话启动时创建内存中的冲突日志。

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

将 IConflictLogAccess 对象传递到 NotifyingChangeApplier 对象

目标提供程序在处理 ProcessChangeBatch 方法期间必须调用 ApplyChanges 方法。请注意调用的 ApplyChanges 必须接受抵触冲突解决策略和 IConflictLogAccess 对象。本示例调用 ApplyChanges,指定抵触冲突解决策略 ApplicationDefined 并传递在 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);
}

实现 SaveConstraintConflict

变更应用方调用 INotifyingChangeApplierTarget2 接口的 SaveConstraintConflict 方法来保存临时冲突。本示例将 INotifyingChangeApplierTarget2 接口添加到实现 KnowledgeSyncProvider 的类。

class ContactsProviderWithConstraintConflicts : KnowledgeSyncProvider
    , INotifyingChangeApplierTarget
    , INotifyingChangeApplierTarget2
    , IChangeDataRetriever

本示例使用 MemoryConflictLog 对象来保存临时冲突,否则引发异常,以此实现 SaveConstraintConflict

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

实现 TryGetDestinationVersion

变更应用方必须能够获取发生冲突的目标项的版本。为此,目标提供程序实现 INotifyingChangeApplierTarget 接口的 TryGetDestinationVersion 方法。不报告约束冲突时,此方法是可选的;否则是必需的。

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

报告约束冲突

约束冲突是在同步的变更应用阶段由目标提供程序检测到的。本示例使用变更单位,因此当变更操作 Create 导致联系人存储区中的标识冲突时,在 SaveChangeWithChangeUnits 方法中通过调用 RecordConstraintConflictForItem 报告约束冲突。

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

在上例中调用的联系人存储区的 CanCreateContact 方法使用私有 DetectIndexCollision 方法检测要创建的联系人是否引起标识冲突。当要创建的联系人包含与现有联系人相同的 name 和 phone number 字段时,发生标识冲突。

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

设置约束冲突的解决操作

目标提供程序指定 ApplicationDefined 作为抵触冲突解决策略时,应用程序在启动同步前必须注册 ItemConstraint 事件的处理程序。

// 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);

ItemConstraint 事件处理程序确定如何解决冲突。本示例向用户显示冲突项,并询问应如何解决冲突。

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

处理约束冲突解决方法

应用程序设置约束冲突解决操作后,变更应用方对与解决方法关联的元数据进行任何必要的变更,调度对目标提供程序的调用,以便可以将该变更应用到目标副本。由于本示例使用变更单位,因此变更应用方调用目标提供程序的 SaveChangeWithChangeUnits 方法。在此方法中处理应用程序可能向用户显示的三个解决方法。

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

完整的提供程序代码

有关本文档中使用的完整提供程序代码,请参见用于报告和解决约束冲突的示例代码

后续步骤

接下来,您可能要实现一个冲突日志,以便在同步会话结束后仍保存冲突信息。有关创建冲突日志的更多信息,请参见记录和管理冲突

请参阅

概念

对常见标准自定义提供程序任务进行编程
检测和解决约束冲突