Search code examples
c#asp.net-corebotframework

Using FormFlow dialog in Bot Framework


I am using Bot Framework SDK v-4.x based on .Net Core. I have couple of dialogs created which I am able to flow through using DialogContext.Begin, DialogContext.End and DialogContext.Continue. Everything works fine, but now I want to implement a FormFlow in the middle of a conversation. I referred this link- https://learn.microsoft.com/en-us/azure/bot-service/dotnet/bot-builder-dotnet-formflow?view=azure-bot-service-3.0

I posted this on Github (https://github.com/MicrosoftDocs/bot-docs/issues/227) and based on the solution this is what I tried-

[Serializable]
public class HelpForm
{
    public string FullName { get; set; }
    public string EmailID { get; set; }
    public string Question { get; set; }
    public DateTime BestTimeToContact { get; set; }
    public List<Priority> Priority { get; set; }
    public static IForm<HelpForm> BuildForm()
    {
        return new FormBuilder<HelpForm>()
            .Message("Please fill out the details as prompted.")
            .Build();
    }
}

public enum Priority
{
    Low,
    Medium,
    High
}

In my OnTurn event of my bot, I am doing something like this-

await Microsoft.Bot.Builder.Classic.Dialogs.Conversation.SendAsync(context, () => FormDialog.FromForm(HelpForm.BuildForm)); //context is of type ITurnContext

This doesn't seem to work and I get a response in the emulator as

Sorry, my bot code is having an issue.

Also, this link- https://github.com/Microsoft/botbuilder-dotnet/wiki/Using-Classic-V3-Dialogs-with-V4-SDK says that Microsoft.Bot.Builder.Classic is not supported in .Net Core. Any help with this please?

Update

Based on Fei's comment, I got the exception details. System.Runtime.Serialization.SerializationException: Type 'System.RuntimeType' in Assembly 'System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e' is not marked as serializable.. Although my HelpForm class is marked with Serializable.

Looking at the metadata for FormFlow class it is not marked with Serializable attribute.

enter image description here

Note sure if that is what the error is about.


Solution

  • Bot Builder V3 FormFlow dialogs can now be used in the context of a V4 bot by using the recently released Bot.Builder.Community.Dialogs.FormFlow library.

    Your HelpForm can be added to a V4 DialogSet in the same way as other V4 ComponentDialogs:

    _dialogs.Add(FormDialog.FromForm(HelpForm.BuildForm));
    

    Here is a more complete example:

    Accessors:

    public class TestEchoBotAccessors
    {
        public TestEchoBotAccessors(ConversationState conversationState)
        {
            ConversationState = conversationState ?? throw new ArgumentNullException(nameof(conversationState));
        }
    
        public ConversationState ConversationState { get; }
        public IStatePropertyAccessor<DialogState> ConversationDialogState { get; set; }
    }
    

    Startup.cs ConfigureServices:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddBot<TestEchoBotBot>(options =>
        {
            IStorage dataStore = new MemoryStorage();
            options.State.Add(new ConversationState(dataStore));
            options.Middleware.Add(new AutoSaveStateMiddleware(options.State.ToArray()));
    
            var secretKey = Configuration.GetSection("botFileSecret")?.Value;
            var botFilePath = Configuration.GetSection("botFilePath")?.Value;
    
            // Loads .bot configuration file and adds a singleton that your Bot can access through dependency injection.
            var botConfig = BotConfiguration.Load(botFilePath ?? @".\TestEchoBot.bot", secretKey);
            services.AddSingleton(sp => botConfig ?? throw new InvalidOperationException($"The .bot config file could not be loaded. ({botConfig})"));
    
            // Retrieve current endpoint.
            var environment = _isProduction ? "production" : "development";
            var service = botConfig.Services.Where(s => s.Type == "endpoint" && s.Name == environment).FirstOrDefault();
            if (!(service is EndpointService endpointService))
            {
                throw new InvalidOperationException($"The .bot file does not contain an endpoint with name '{environment}'.");
            }
    
            options.CredentialProvider = new SimpleCredentialProvider(endpointService.AppId, endpointService.AppPassword);
        });
    
        services.AddSingleton(sp =>
        {
            var options = sp.GetRequiredService<IOptions<BotFrameworkOptions>>().Value;
            var conversationState = options.State.OfType<ConversationState>().FirstOrDefault();
            var accessors = new TestEchoBotAccessors(conversationState)
            {
                ConversationDialogState = conversationState.CreateProperty<DialogState>("DialogState")
            };
            return accessors;
        });
    }
    

    Bot code:

    public class TestEchoBotBot : IBot
    {
        private readonly TestEchoBotAccessors _accessors;
        private DialogSet _dialogs;
    
        public TestEchoBotBot(TestEchoBotAccessors accessors, ILoggerFactory loggerFactory)
        {
            if (loggerFactory == null)
            {
                throw new System.ArgumentNullException(nameof(loggerFactory));
            }
    
            _dialogs = new DialogSet(accessors.ConversationDialogState);
            _dialogs.Add(FormDialog.FromForm(HelpForm.BuildForm));
            _accessors = accessors ?? throw new System.ArgumentNullException(nameof(accessors));
        }
    
        public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
        {
            if (turnContext.Activity.Type == ActivityTypes.Message)
            {
                var dialogContext = await _dialogs.CreateContextAsync(turnContext, cancellationToken);
                if (turnContext.Activity.Text?.ToUpper() == "HELP")
                {
                    await dialogContext.BeginDialogAsync(typeof(HelpForm).Name, null, cancellationToken);
                }
                else
                {
                    var dialogResult = await dialogContext.ContinueDialogAsync(cancellationToken);
                    if ((dialogResult.Status == DialogTurnStatus.Cancelled || dialogResult.Status == DialogTurnStatus.Empty))
                    {
                        var responseMessage = $"You sent '{turnContext.Activity.Text}'\n";
                        await turnContext.SendActivityAsync(responseMessage);
                    }
                }                
            }
        }
    }