standardize FormDialog construction

This commit is contained in:
Will Portnoy 2016-03-26 12:21:14 -07:00
Родитель d21c9181fc
Коммит ceb6e6afc9
17 изменённых файлов: 202 добавлений и 264 удалений

Просмотреть файл

@ -42,7 +42,7 @@ namespace Microsoft.Bot.Builder.Form.Advanced
/// </summary>
/// <typeparam name="T">Form state.</typeparam>
public class Confirmation<T> : Field<T>
where T : class, new()
where T : class
{
/// <summary>
/// Construct a confirmation.

Просмотреть файл

@ -395,7 +395,7 @@ namespace Microsoft.Bot.Builder.Form.Advanced
}
public class FieldReflector<T> : Field<T>
where T : class, new()
where T : class
{
public FieldReflector(string name, IForm<T> form, bool ignoreAnnotations = false)
: base(name, FieldRole.Value, form)
@ -832,7 +832,7 @@ namespace Microsoft.Bot.Builder.Form.Advanced
}
public class Conditional<T> : FieldReflector<T>
where T : class, new()
where T : class
{
public Conditional(string name, IForm<T> form, ConditionalDelegate<T> condition, bool ignoreAnnotations = false)
: base(name, form, ignoreAnnotations)

Просмотреть файл

@ -38,7 +38,7 @@ using Microsoft.Bot.Builder.Form.Advanced;
namespace Microsoft.Bot.Builder.Form
{
internal sealed class Form<T> : IForm<T>
where T : class, new()
where T : class
{
internal readonly bool _ignoreAnnotations;
internal readonly FormConfiguration _configuration;

Просмотреть файл

@ -41,7 +41,7 @@ using Microsoft.Bot.Builder.Form.Advanced;
namespace Microsoft.Bot.Builder.Form
{
public sealed class FormBuilder<T> : IFormBuilder<T>
where T : class, new()
where T : class
{
private readonly Form<T> _form;

Просмотреть файл

@ -43,8 +43,19 @@ using Microsoft.Bot.Builder.Form.Advanced;
namespace Microsoft.Bot.Builder.Form
{
internal static class FormStatics
public static class FormDialog
{
public static IFormDialog<T> FromType<T>() where T : class, new()
{
return new FormDialog<T>(new T());
}
public static IFormDialog<T> FromForm<T>(MakeForm<T> makeForm) where T : class, new()
{
return new FormDialog<T>(new T(), makeForm);
}
#region IForm<T> statics
#if DEBUG
internal static bool DebugRecognizers = false;
@ -52,6 +63,11 @@ namespace Microsoft.Bot.Builder.Form
#endregion
}
[Flags]
public enum FormOptions { None, PromptInStart };
public delegate IForm<T> MakeForm<T>();
/// <summary>
/// Form dialog manager for to fill in your state.
/// </summary>
@ -65,51 +81,75 @@ namespace Microsoft.Bot.Builder.Form
/// </remarks>
[Serializable]
public sealed class FormDialog<T> : IFormDialog<T>, ISerializable
where T : class, new()
where T : class
{
private readonly InitialState<T> _initialState;
private readonly MakeForm _makeForm;
private readonly IForm<T> _form;
private FormState _formState;
private T _state;
private IRecognize<T> _commands;
// constructor arguments
private readonly T _state;
private readonly MakeForm<T> _makeForm;
private readonly IEnumerable<Models.EntityRecommendation> _entities;
private readonly FormOptions _options;
public delegate IForm<T> MakeForm();
// instantiated in constructor, saved when serialized
private readonly FormState _formState;
// instantiated in constructor, re-instantiated when deserialized
private readonly IForm<T> _form;
private readonly IRecognize<T> _commands;
private static IForm<T> MakeDefaultForm()
{
return new FormBuilder<T>().AddRemainingFields().Build();
}
/// <summary>
/// Construct a form.
/// </summary>
/// <param name="id">Unique dialog id to register with dialog system.</param>
public FormDialog(MakeForm makeForm = null, InitialState<T> initialState = null)
public FormDialog(T state, MakeForm<T> makeForm = null, FormOptions options = FormOptions.None, IEnumerable < Models.EntityRecommendation> entities = null, CultureInfo cultureInfo = null)
{
_initialState = initialState;
_makeForm = makeForm ?? (MakeForm)MakeDefaultForm;
_form = _makeForm();
makeForm = makeForm ?? MakeDefaultForm;
entities = entities ?? Enumerable.Empty<Models.EntityRecommendation>();
cultureInfo = cultureInfo ?? CultureInfo.InvariantCulture;
// constructor arguments
Field.SetNotNull(out this._state, nameof(state), state);
Field.SetNotNull(out this._makeForm, nameof(makeForm), makeForm);
Field.SetNotNull(out this._entities, nameof(entities), entities);
this._options = options;
// make our form
var form = _makeForm();
// instantiated in constructor, saved when serialized
this._formState = new FormState(form.Steps.Count, cultureInfo);
// instantiated in constructor, re-instantiated when deserialized
this._form = form;
this._commands = this._form.BuildCommandRecognizer();
}
private FormDialog(SerializationInfo info, StreamingContext context)
{
Microsoft.Bot.Builder.Field.SetNotNullFrom(out this._makeForm, nameof(this._makeForm), info);
this._formState = info.GetValue<FormState>(nameof(this._formState));
this._state = info.GetValue<T>(nameof(this._state));
// constructor arguments
Field.SetNotNullFrom(out this._state, nameof(this._state), info);
Field.SetNotNullFrom(out this._makeForm, nameof(this._makeForm), info);
Field.SetNotNullFrom(out this._entities, nameof(this._entities), info);
this._options = info.GetValue<FormOptions>(nameof(this._options));
_form = _makeForm();
// instantiated in constructor, saved when serialized
Field.SetNotNullFrom(out this._formState, nameof(this._formState), info);
// instantiated in constructor, re-instantiated when deserialized
this._form = _makeForm();
this._commands = this._form.BuildCommandRecognizer();
}
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue(nameof(this._makeForm), this._makeForm);
info.AddValue(nameof(this._formState), this._formState);
// constructor arguments
info.AddValue(nameof(this._state), this._state);
info.AddValue(nameof(this._makeForm), this._makeForm);
info.AddValue(nameof(this._entities), this._entities);
info.AddValue(nameof(this._options), this._options);
// instantiated in constructor, saved when serialized
info.AddValue(nameof(this._formState), this._formState);
}
#region IForm<T> implementation
@ -122,84 +162,44 @@ namespace Microsoft.Bot.Builder.Form
async Task IDialog.StartAsync(IDialogContext context)
{
bool skipFields = false;
if (this._initialState == null)
var entityGroups = (from entity in this._entities group entity by entity.Type);
foreach (var entityGroup in entityGroups)
{
if (context.UserData.TryGetValue(typeof(T).Name, out _state))
var step = _form.Step(entityGroup.Key);
if (step != null)
{
skipFields = true;
}
else
{
_state = new T();
}
}
else
{
_state = this._initialState.State;
if (_state == null) _state = new T();
skipFields = true;
}
// TODO: Hook up culture state in form
_formState = new FormState(_form.Steps.Count, CultureInfo.InvariantCulture);
if (this._initialState != null && this._initialState.Entities != null)
{
var entities = (from entity in this._initialState.Entities group entity by entity.Type);
foreach (var entityGroup in entities)
{
var step = _form.Step(entityGroup.Key);
if (step != null)
_formState.Step = _form.StepIndex(step);
_formState.StepState = null;
var builder = new StringBuilder();
foreach (var entity in entityGroup)
{
_formState.Step = _form.StepIndex(step);
_formState.StepState = null;
var builder = new StringBuilder();
foreach (var entity in entityGroup)
{
builder.Append(entity.Entity);
builder.Append(' ');
}
var input = builder.ToString();
string feedback;
string prompt = step.Start(context, _state, _formState);
var matches = MatchAnalyzer.Coalesce(step.Match(context, _state, _formState, input, out prompt), input);
if (MatchAnalyzer.IsFullMatch(input, matches, 0.5))
{
// TODO: In the case of clarification I could
// 1) Go through them while supporting only quit or back and reset
// 2) Drop them
// 3) Just pick one (found in form.StepState, but that is opaque here)
var result = await step.ProcessAsync(context, _state, _formState, input, matches);
feedback = result.Feedback;
prompt = result.Prompt;
}
else
{
_formState.SetPhase(StepPhase.Ready);
}
builder.Append(entity.Entity);
builder.Append(' ');
}
}
_formState.Step = 0;
_formState.StepState = null;
}
if (skipFields)
{
// Mark all fields with values as completed.
for (var i = 0; i < _form.Steps.Count; ++i)
{
var step = _form.Steps[i];
if (step.Type == StepType.Field)
var input = builder.ToString();
string feedback;
string prompt = step.Start(context, _state, _formState);
var matches = MatchAnalyzer.Coalesce(step.Match(context, _state, _formState, input, out prompt), input);
if (MatchAnalyzer.IsFullMatch(input, matches, 0.5))
{
if (!step.Field.IsUnknown(_state))
{
_formState.Phases[i] = StepPhase.Completed;
}
// TODO: In the case of clarification I could
// 1) Go through them while supporting only quit or back and reset
// 2) Drop them
// 3) Just pick one (found in form.StepState, but that is opaque here)
var result = await step.ProcessAsync(context, _state, _formState, input, matches);
feedback = result.Feedback;
prompt = result.Prompt;
}
else
{
_formState.SetPhase(StepPhase.Ready);
}
}
}
if (this._initialState != null && this._initialState.PromptInStart)
_formState.Step = 0;
_formState.StepState = null;
if (this._options.HasFlag(FormOptions.PromptInStart))
{
await MessageReceived(context, null);
}
@ -596,3 +596,16 @@ namespace Microsoft.Bot.Builder.Form
}
}
namespace Microsoft.Bot.Builder.Models
{
[Serializable]
public partial class EntityRecommendation
{
}
[Serializable]
public partial class IntentRecommendation
{
}
}

Просмотреть файл

@ -49,7 +49,7 @@ namespace Microsoft.Bot.Builder.Form
public static partial class Extension
{
internal static IStep<T> Step<T>(this IForm<T> form, string name) where T : class, new()
internal static IStep<T> Step<T>(this IForm<T> form, string name) where T : class
{
IStep<T> result = null;
foreach (var step in form.Steps)
@ -63,7 +63,7 @@ namespace Microsoft.Bot.Builder.Form
return result;
}
internal static int StepIndex<T>(this IForm<T> form, IStep<T> step) where T : class, new()
internal static int StepIndex<T>(this IForm<T> form, IStep<T> step) where T : class
{
var index = -1;
for (var i = 0; i < form.Steps.Count; ++i)
@ -77,7 +77,7 @@ namespace Microsoft.Bot.Builder.Form
return index;
}
internal static IRecognize<T> BuildCommandRecognizer<T>(this IForm<T> form) where T : class, new()
internal static IRecognize<T> BuildCommandRecognizer<T>(this IForm<T> form) where T : class
{
var field = new Field<T>("__commands__", FieldRole.Value, form);
field.Prompt(new Prompt(""));

Просмотреть файл

@ -38,7 +38,6 @@ using Microsoft.Bot.Builder.Form.Advanced;
namespace Microsoft.Bot.Builder.Form
{
public interface IFormBuilder<T>
where T : class, new()
{
/// <summary>
/// Build the form based on the methods called on the builder.

Просмотреть файл

@ -84,7 +84,6 @@ namespace Microsoft.Bot.Builder.Form
/// If you do not take explicit control, the steps will be executed in the order defined in the form state class with a final confirmation.
/// </remarks>
public interface IFormDialog<T> : IDialog
where T : class, new()
{
/// <summary>
/// The form specification.

Просмотреть файл

@ -1,67 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Builder SDK Github:
// https://github.com/Microsoft/BotBuilder
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
using System;
namespace Microsoft.Bot.Builder.Form
{
/// <summary>
/// Initial state for a Microsoft.Bot.Builder.Form.Form.
/// </summary>
/// <remarks>
/// If a parent dialog wants to pass in the initial state of the form, you would use this structure.
/// It includes both the state and optionally initial entities from a LUIS dialog that will be used to
/// initially populate the form state.
/// </remarks>
[Serializable]
public class InitialState<T>
{
/// <summary>
/// Default form state.
/// </summary>
public T State { get; set; }
/// <summary>
/// LUIS entities to put into state.
/// </summary>
/// <remarks>
/// In order to set a field in the form state, the Entity must be named with the path to the field in the form state.
/// </remarks>
public Models.EntityRecommendation[] Entities { get; set; }
/// <summary>
/// Whether this form should prompt the user when started.
/// </summary>
public bool PromptInStart { get; set; }
}
}

Просмотреть файл

@ -344,7 +344,7 @@ namespace Microsoft.Bot.Builder.Form.Advanced
/// </summary>
/// <typeparam name="T">Form state.</typeparam>
public abstract class RecognizePrimitive<T> : IRecognize<T>
where T : class, new()
where T : class
{
/// <summary>
@ -462,7 +462,7 @@ namespace Microsoft.Bot.Builder.Form.Advanced
/// </summary>
/// <typeparam name="T">Form state.</typeparam>
public sealed class RecognizeBool<T> : RecognizePrimitive<T>
where T : class, new()
where T : class
{
/// <summary>
/// Construct a boolean recognizer for a field.
@ -522,7 +522,7 @@ namespace Microsoft.Bot.Builder.Form.Advanced
/// </summary>
/// <typeparam name="T">Form state.</typeparam>
public sealed class RecognizeString<T> : RecognizePrimitive<T>
where T : class, new()
where T : class
{
/// <summary>
/// Construct a string recognizer for a field.
@ -567,7 +567,7 @@ namespace Microsoft.Bot.Builder.Form.Advanced
/// </summary>
/// <typeparam name="T">Form state.</typeparam>
public sealed class RecognizeNumber<T> : RecognizePrimitive<T>
where T : class, new()
where T : class
{
/// <summary>
/// Construct a numeric recognizer for a field.
@ -631,7 +631,7 @@ namespace Microsoft.Bot.Builder.Form.Advanced
/// </summary>
/// <typeparam name="T"></typeparam>
public sealed class RecognizeDouble<T> : RecognizePrimitive<T>
where T : class, new()
where T : class
{
/// <summary>
@ -696,7 +696,7 @@ namespace Microsoft.Bot.Builder.Form.Advanced
/// Expressions recognized are based on the C# nuget package Chronic.
/// </remarks>
public sealed class RecognizeDateTime<T> : RecognizePrimitive<T>
where T : class, new()
where T : class
{
/// <summary>
/// Construct a date/time recognizer.

Просмотреть файл

@ -108,7 +108,7 @@ namespace Microsoft.Bot.Builder.Form
}
}
#if DEBUG
if (FormStatics.DebugRecognizers)
if (FormDialog.DebugRecognizers)
{
MatchAnalyzer.PrintMatches(matches, 2);
}

Просмотреть файл

@ -110,7 +110,6 @@
<None Include="Form\ILocalizer.cs" />
<Compile Include="Form\IFormBuilder.cs" />
<Compile Include="Form\IForm.cs" />
<Compile Include="Form\InitialState.cs" />
<Compile Include="Form\IStep.cs" />
<Compile Include="Form\Language.cs" />
<Compile Include="Form\MatchAnalyzer.cs" />
@ -141,7 +140,7 @@
</PropertyGroup>
<Error Condition="!Exists('..\packages\Microsoft.Net.Compilers.1.1.1\build\Microsoft.Net.Compilers.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Net.Compilers.1.1.1\build\Microsoft.Net.Compilers.props'))" />
</Target>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>

Просмотреть файл

@ -18,7 +18,7 @@ namespace Microsoft.Bot.Sample.AnnotatedSandwichBot
{
internal static IFormDialog<SandwichOrder> MakeRoot()
{
return new FormDialog<SandwichOrder>(SandwichOrder.Form);
return FormDialog.FromForm(SandwichOrder.Form);
}
/// <summary>

Просмотреть файл

@ -47,7 +47,7 @@ namespace Microsoft.Bot.Sample.PizzaBot
internal static IDialog MakeRoot()
{
return new PizzaOrderDialog(initialState => new FormDialog<PizzaOrder>(MakeForm, initialState));
return new PizzaOrderDialog(MakeForm);
}
/// <summary>

Просмотреть файл

@ -15,9 +15,9 @@ namespace Microsoft.Bot.Sample.PizzaBot
[Serializable]
public class PizzaOrderDialog : LuisDialog
{
private readonly Func<InitialState<PizzaOrder>, IFormDialog<PizzaOrder>> MakePizzaForm;
private readonly MakeForm<PizzaOrder> MakePizzaForm;
internal PizzaOrderDialog(Func<InitialState<PizzaOrder>, IFormDialog<PizzaOrder>> makePizzaForm)
internal PizzaOrderDialog(MakeForm<PizzaOrder> makePizzaForm)
{
this.MakePizzaForm = makePizzaForm;
}
@ -45,7 +45,6 @@ namespace Microsoft.Bot.Sample.PizzaBot
[LuisIntent("UseCoupon")]
public async Task ProcessPizzaForm(IDialogContext context, LuisResult result)
{
var initialState = new InitialState<PizzaOrder>();
var entities = new List<EntityRecommendation>(result.Entities);
if (!entities.Any((entity) => entity.Type == "Kind"))
{
@ -68,11 +67,8 @@ namespace Microsoft.Bot.Sample.PizzaBot
}
}
}
initialState.Entities = entities.ToArray();
initialState.State = null;
initialState.PromptInStart = true;
var pizzaForm = this.MakePizzaForm(initialState);
var pizzaForm = new FormDialog<PizzaOrder>(new PizzaOrder(), this.MakePizzaForm, FormOptions.PromptInStart, entities);
context.Call<PizzaOrder>(pizzaForm, PizzaFormComplete);
}

Просмотреть файл

@ -1,72 +1,72 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Description;
using Microsoft.Bot.Connector;
using Microsoft.Bot.Connector.Utilities;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Form;
using Newtonsoft.Json;
namespace Microsoft.Bot.Sample.SimpleSandwichBot
{
[BotAuthentication]
public class MessagesController : ApiController
{
internal static IFormDialog<SandwichOrder> MakeRoot()
{
return new FormDialog<SandwichOrder>(SandwichOrder.Form);
}
/// <summary>
/// POST: api/Messages
/// receive a message from a user and reply to it
/// </summary>
public async Task<Message> Post([FromBody]Message message)
{
if (message.Type == "Message")
{
return await CompositionRoot.SendAsync(message, MakeRoot);
}
else
{
return HandleSystemMessage(message);
}
}
private Message HandleSystemMessage(Message message)
{
if (message.Type == "Ping")
{
Message reply = message.CreateReplyMessage();
reply.Type = "Ping";
return reply;
}
else if (message.Type == "DeleteUserData")
{
// Implement user deletion
// If we handle user deletion, return a real message
}
else if (message.Type == "BotAddedToConversation")
{
}
else if (message.Type == "BotRemovedFromConversation")
{
}
else if (message.Type == "UserAddedToConversation")
{
}
else if (message.Type == "UserRemovedFromConversation")
{
}
else if (message.Type == "EndOfConversation")
{
}
return null;
}
}
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Description;
using Microsoft.Bot.Connector;
using Microsoft.Bot.Connector.Utilities;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Form;
using Newtonsoft.Json;
namespace Microsoft.Bot.Sample.SimpleSandwichBot
{
[BotAuthentication]
public class MessagesController : ApiController
{
internal static IFormDialog<SandwichOrder> MakeRoot()
{
return FormDialog.FromForm(SandwichOrder.Form);
}
/// <summary>
/// POST: api/Messages
/// receive a message from a user and reply to it
/// </summary>
public async Task<Message> Post([FromBody]Message message)
{
if (message.Type == "Message")
{
return await CompositionRoot.SendAsync(message, MakeRoot);
}
else
{
return HandleSystemMessage(message);
}
}
private Message HandleSystemMessage(Message message)
{
if (message.Type == "Ping")
{
Message reply = message.CreateReplyMessage();
reply.Type = "Ping";
return reply;
}
else if (message.Type == "DeleteUserData")
{
// Implement user deletion
// If we handle user deletion, return a real message
}
else if (message.Type == "BotAddedToConversation")
{
}
else if (message.Type == "BotRemovedFromConversation")
{
}
else if (message.Type == "UserAddedToConversation")
{
}
else if (message.Type == "UserRemovedFromConversation")
{
}
else if (message.Type == "EndOfConversation")
{
}
return null;
}
}
}

Просмотреть файл

@ -133,16 +133,15 @@ namespace Microsoft.Bot.Builder.FormTest
.Build();
}
public static void Call<T>(IDialogContext context, CallDialog<Choices> root, FormDialog<T>.MakeForm makeForm) where T : class, new()
public static void Call<T>(IDialogContext context, CallDialog<Choices> root, MakeForm<T> makeForm) where T : class, new()
{
var initialState = new InitialState<T>() { PromptInStart = true };
var form = new FormDialog<T>(makeForm, initialState);
var form = new FormDialog<T>(new T(), makeForm, options: FormOptions.PromptInStart);
context.Call<T>(form, root.CallChild);
}
static void Main(string[] args)
{
var choiceForm = new FormDialog<Choices>();
var choiceForm = FormDialog.FromType<Choices>();
var callDebug = new CallDialog<Choices>(choiceForm, async (root, context, result) =>
{
Choices choices;