У вас есть несколько опций в зависимости от того, что вы хотите сделать в функции валидатора и где вы хотите поместить код, управляющий стеком диалога.
Опция 1: вернуть false
Ваша первая возможность вытолкнуть диалоги из стека будет в самой функции валидатора, как я упоминал в комментариях.
if (promptContext.AttemptCount > 3)
{
var dc = await BotUtil.Dialogs.CreateContextAsync(promptContext.Context, cancellationToken);
await dc.CancelAllDialogsAsync(cancellationToken);
return false;
}
Вы были правы, опасаясь этого, потому что это на самом деле может вызвать проблемыесли вы не делаете это правильно.SDK не ожидает, что вы будете манипулировать стеком диалога в функции валидатора, поэтому вам необходимо знать, что происходит, когда функция валидатора возвращается, и действовать соответственно.
Опция 1.1: отправка действия
Вы можете увидеть в исходном коде , что подсказка будет пытаться выполнить повторную репликацию без проверки того, находится ли подсказка в стеке диалога:
if (!dc.Context.Responded)
{
await OnPromptAsync(dc.Context, state, options, true, cancellationToken).ConfigureAwait(false);
}
Это означает, что даже если вы очистите стек диалога внутри функции валидатора, после этого вы все равно попытаетесь выполнить повторную репликацию, когда вы вернете false
.Мы не хотим, чтобы это произошло, потому что диалог уже был отменен, и если бот задает вопрос, на который он не будет принимать ответы, это будет выглядеть плохо и сбить с толку пользователя.Тем не менее, этот исходный код дает подсказку о том, как избежать повторной компоновки.Он будет перепроверен, только если TurnContext.Responded
равен false
.Вы можете установить его на true
, отправив действие.
Опция 1.1.1: отправить сообщение активность
Имеет смысл сообщить пользователю, что он использовал все своипопыток, и если вы отправите пользователю такое сообщение в своей функции валидатора, вам не придется беспокоиться о каких-либо нежелательных автоматических повторных репликах:
await promptContext.Context.SendActivityAsync("Cancelling all dialogs...");
Опция 1.1.2: отправка активности события
Если вы не хотите отображать реальное сообщение пользователю, вы можете отправить невидимое событие, которое не будет отображаться в диалоге.При этом для TurnContext.Responded
будет true
:
await promptContext.Context.SendActivityAsync(new Activity(ActivityTypes.Event));
Опция 1.2: аннулировать приглашение
Возможно, нам не нужно будет избегать вызова приглашения на его OnPromptAsync
, если конкретныйТип приглашения позволяет избежать перепроверки внутри OnPromptAsync
.Опять же, посмотрев на исходный код, но на этот раз в TextPrompt.cs , мы можем увидеть, где OnPromptAsync
выполняет его перекомпоновку:
if (isRetry && options.RetryPrompt != null)
{
await turnContext.SendActivityAsync(options.RetryPrompt, cancellationToken).ConfigureAwait(false);
}
else if (options.Prompt != null)
{
await turnContext.SendActivityAsync(options.Prompt, cancellationToken).ConfigureAwait(false);
}
Так что еслимы не хотим отправлять какие-либо действия пользователю (видимые или иные), мы можем запретить повторное сопоставление текстового приглашения, просто установив для его свойств Prompt
и RetryPrompt
значение null:
promptContext.Options.Prompt = null;
promptContext.Options.RetryPrompt = null;
Вариант 2: возврат true
Вторая возможность отменить диалоги, когда мы поднимаемся вверх по стеку вызовов из функции валидатора, находится на следующем шаге водопада, как вы упомянули в своем вопросе.Это может быть вашим лучшим вариантом, потому что он наименее хакерский: он не зависит от какого-либо особого понимания внутреннего кода SDK, который может быть изменен.В этом случае вся ваша функция валидатора может быть такой простой:
private Task<bool> ValidateAsync(PromptValidatorContext<string> promptContext, CancellationToken cancellationToken)
{
if (promptContext.AttemptCount > 3 || IsCorrectPassword(promptContext.Context.Activity.Text))
{
// valid user input
// or continue to next step anyway because of too many attempts
return Task.FromResult(true);
}
// invalid user input
// when there haven't been too many attempts
return Task.FromResult(false);
}
Обратите внимание, что мы используем метод с именем IsCorrectPassword
, чтобы определить, верен ли пароль.Это важно, потому что эта опция зависит от повторного использования этой функциональности на следующем этапе водопада.Вы упоминали о необходимости сохранять информацию в TurnState
, но это не нужно, поскольку все, что нам нужно знать, уже находится в контексте поворота.Проверка основана на тексте действия, поэтому мы можем просто проверить этот же текст снова на следующем шаге.
Опция 2.1: использовать WaterfallStepContext.Context.Activity.Text
Текст, введенный пользователем, будет по-прежнемубыть доступным для вас в WaterfallStepContext.Context.Activity.Text
, чтобы ваш следующий шаг водопада мог выглядеть следующим образом:
async (stepContext, cancellationToken) =>
{
if (IsCorrectPassword(stepContext.Context.Activity.Text))
{
return await stepContext.NextAsync(null, cancellationToken);
}
else
{
await stepContext.Context.SendActivityAsync("Cancelling all dialogs...");
return await stepContext.CancelAllDialogsAsync(cancellationToken);
}
},
Опция 2.2: используйте WaterfallStepContext.Result
Контексты шага водопада имеют встроенное свойство Result
это относится к результату предыдущего шага.В случае текстового приглашения это будет строка, возвращенная этим приглашением.Вы можете использовать его так:
if (IsCorrectPassword((string)stepContext.Result))
Вариант 3: выбросить исключение
Продвигаясь дальше вверх по стеку вызовов, вы можете обрабатывать вещи в обработчике сообщений, который первоначально вызывал DialogContext.ContinueDialogAsync
, вызывая исключение в вашей функции валидатора, как CameronL, упомянутый в удаленной части их ответа. Хотя обычно считается плохой практикой использование исключений для запуска преднамеренных путей кода, это очень похоже на то, как работали ограничения повторов в Bot Builder v3, который вы упомянули, желая повторить.
Вариант 3.1: использовать базу Exception
тип
Вы можете выбросить только обычное исключение. Чтобы было проще отличить это исключение от других исключений при его обнаружении, вы можете дополнительно включить некоторые метаданные в свойство Source
исключения:
if (promptContext.AttemptCount > 3)
{
throw new Exception("Too many attempts") { Source = ID };
}
Тогда вы можете поймать это так:
try
{
await dc.ContinueDialogAsync(cancellationToken);
}
catch (Exception ex)
{
if (ex.Source == TestDialog.ID)
{
await turnContext.SendActivityAsync("Cancelling all dialogs...");
await dc.CancelAllDialogsAsync(cancellationToken);
}
else
{
throw ex;
}
}
Опция 3.2: использовать производный тип исключения
Если вы определяете свой собственный тип исключения, вы можете использовать его, чтобы перехватить только это конкретное исключение.
public class TooManyAttemptsException : Exception
Вы можете бросить это так:
throw new TooManyAttemptsException();
Тогда вы можете поймать это так:
try
{
await dc.ContinueDialogAsync(cancellationToken);
}
catch (TooManyAttemptsException)
{
await turnContext.SendActivityAsync("Cancelling all dialogs...");
await dc.CancelAllDialogsAsync(cancellationToken);
}