TelegramUpdater.FillMyForm 1.0.1-alpha

This is a prerelease version of TelegramUpdater.FillMyForm.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package TelegramUpdater.FillMyForm --version 1.0.1-alpha                
NuGet\Install-Package TelegramUpdater.FillMyForm -Version 1.0.1-alpha                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="TelegramUpdater.FillMyForm" Version="1.0.1-alpha" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add TelegramUpdater.FillMyForm --version 1.0.1-alpha                
#r "nuget: TelegramUpdater.FillMyForm, 1.0.1-alpha"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install TelegramUpdater.FillMyForm as a Cake Addin
#addin nuget:?package=TelegramUpdater.FillMyForm&version=1.0.1-alpha&prerelease

// Install TelegramUpdater.FillMyForm as a Cake Tool
#tool nuget:?package=TelegramUpdater.FillMyForm&version=1.0.1-alpha&prerelease                

TelegramUpdater, FMF ( Fill my form )

This is an extension package for TelegramUpdater to help you fill a form in a blink of an eye.

How ?

FMF uses Channels feature of TelegramUpdater to open a realtime channel between your bot and a target user.

FMF will ask from a user and the user will respond ( probably ). and meanwhile your form is getting filled.

Let's Start

I'm gonna start with a simple form that has 3 felids: FirsName, LastName and Age.

First of all, create your form using a normal class.

// MySimpleForm.cs
internal class MySimpleForm
{
    public string FirstName { get; set; } = null!;

    public string? LastName { get; set; } // can be null, It's Nullable!

    public int Age { get; set; }

    public override string ToString()
    {
        return string.Format("{0} {1}, {2} years old.", FirstName, LastName?? "", Age);
    }
}

! Considerations

  • Target properties should be: public, Readable and Writeable ( both get and set )

A useable form should implement IForm interface! But AbstractForm is what you need.

Make your form a sub-class of AbstractForm and implement abstract methods.

// MySimpleForm.cs
internal class MySimpleForm : AbstractForm
{
    public string FirstName { get; set; } = null!;

    public string? LastName { get; set; } // can be null, It's Nullable!

    public int Age { get; set; }

    public override string ToString()
    {
        return string.Format("{0} {1}, {2} years old.", FirstName, LastName?? "", Age);
    }

    public override Task OnBeginAskAsync(
        IUpdater updater,
        User askingFrom,
        string propertyName,
        CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public override Task OnSuccessAsync(
        RawContainer? container,
        User askingFrom,
        string propertyName,
        OnSuccessContext onSuccessContext,
        CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
}

The things that are less requirements, are to ask the user and notify him if input was successful

This things are possible using OnBeginAskAsync and OnSuccessAsync methods.

There are more methods like these to handle different conditions. but these are less required methods.

Let's apply a simple implementation for OnBeginAskAsync.

public override async Task OnBeginAskAsync(IUpdater updater,
                                            User askingFrom,
                                            string propertyName,
                                            CancellationToken cancellationToken)
{
    await updater.BotClient.SendTextMessageAsync(
        askingFrom.Id, $"Please send me a value for {propertyName}",
        replyMarkup: new ForceReplyMarkup(),
        cancellationToken: cancellationToken);
}

You can customize this the way you like. ( different texts based on property name )

For now i don't want to say anything on partial successes. Therefor:

public override Task OnSuccessAsync(RawContainer? container,
                                    User askingFrom,
                                    string propertyName,
                                    OnSuccessContext onSuccessContext,
                                    CancellationToken cancellationToken)
{
    return Task.CompletedTask;
}

Create an scoped update handler for /form Command.

// FormHandler.cs
using Telegram.Bot.Types;
using TelegramUpdater;
using TelegramUpdater.FillMyForm;
using TelegramUpdater.UpdateContainer;
using TelegramUpdater.UpdateHandlers.ScopedHandlers.ReadyToUse;

namespace QuickExample;

[ApplyFilter(typeof(FormStartFilter))]
internal class FormHandler : ScopedMessageHandler
{
    protected override async Task HandleAsync(IContainer<Message> updateContainer)
    {
        // Create a FormFiller using your form ( MySimpleForm )
        var filler = new FormFiller<MySimpleForm>();

        // Start filling the form
        var ok = await filler.FillAsync(updateContainer.Sender()!, updateContainer);

        if (ok) // Filled with success.
        {
            await updateContainer.Response($"Thank you, {filler.Form}");
        }
        else // Form is broken.
        {
            await updateContainer.Response($"Please try again later.");
        }
    }
}

// Create filter.
class FormStartFilter : Filter<Message>
{
    // Filter message text like /form
    public FormStartFilter() : base(FilterCutify.OnCommand("form")) { }
}

Go to Program.cs and setup your Updater.

// Program.cs
using QuickExample;
using Telegram.Bot;
using TelegramUpdater;

await new Updater(new TelegramBotClient("BotToken"))
    .AddScopedMessage<FormHandler>()
    .StartAsync();

Run the app and send /form command to your bot.

Add a cancel trigger

Cancel triggers are responsible for cancel request like /cancel.

Simplest way to add a cancel trigger for /cancel is to use MessageCancelTextTrigger (which is a ready-to-use cancel trigger) and FormPropertyAttribute.

Place attribute on your properties.

// MySimpleForm.cs
// -- sniff --

internal class MySimpleForm : AbstractForm
{
    [FormProperty(CancelTriggerType = typeof(MessageCancelTextTrigger))]
    public string FirstName { get; set; } = null!;

    [FormProperty(CancelTriggerType = typeof(MessageCancelTextTrigger))]
    public string? LastName { get; set; } // can be null, It's Nullable!

    [FormProperty(CancelTriggerType = typeof(MessageCancelTextTrigger))]
    public int Age { get; set; }

// -- sniff --

Run the app again and try sending /cancel while filling a form.

Default cancel trigger

You can also use a default cancel trigger which will be used for all of properties if they has no any cancel triggers already.

var filler = new FormFiller<MySimpleForm>(
    defaultCancelTrigger: new MessageCancelTextTrigger());

Make a property Required

Simply use [Required] from System.ComponentModel.DataAnnotations.

// MySimpleForm.cs
// -- sniff --

internal class MySimpleForm : AbstractForm
{
    [Required]
    [FormProperty(CancelTriggerType = typeof(MessageCancelTextTrigger))]
    public string FirstName { get; set; } = null!;

    [FormProperty(CancelTriggerType = typeof(MessageCancelTextTrigger))]
    public string? LastName { get; set; } // can be null, It's Nullable!

    [Required]
    [FormProperty(CancelTriggerType = typeof(MessageCancelTextTrigger))]
    public int Age { get; set; }

// -- sniff --

Now try using /cancel on required properties and see the form fails.

Other validations

You can set other limitation for user inputs. Eg: MaxLength for string.

// MySimpleForm.cs
// -- sniff --

[Required]
[MinLength(3)]
[MaxLength(32)]
public string FirstName { get; set; } = null!;

[MinLength(3)]
[MaxLength(32)]
public string? LastName { get; set; } // can be null, It's Nullable!

[Required]
[Range(13, 120)]
public int Age { get; set; }

// -- sniff --

At this point you better implement another method to handle invalid inputs response. The method is OnValidationErrorAsync.

I implemented it this way:

public override async Task OnValidationErrorAsync(IUpdater updater,
                                            ShiningInfo<long, Update>? shiningInfo,
                                            User user,
                                            string propertyName,
                                            ValidationErrorContext validationErrorContext,
                                            CancellationToken cancellationToken)
{
    if (validationErrorContext.RequiredItemNotSupplied)
    {
        await updater.BotClient.SendTextMessageAsync(
            user.Id, $"{propertyName} was required! You can't just leave it.");
    }
    else
    {
        await updater.BotClient.SendTextMessageAsync(
            user.Id,
            $"You input is invalid for {propertyName}.\n" +
            string.Join("\n", validationErrorContext.ValidationResults.Select(
                x=> x.ErrorMessage)));
    }
}

Go ahead! run the bot, try some invalid stuff and watch.

Screenshot-1

Add retry options

You can add retry option for a property. it means the bot will try again if something gets invalid.

[Required]
[MinLength(3)]
[MaxLength(32)]
[FillPropertyRetry(FillingError.ValidationError, 2)]
public string FirstName { get; set; } = null!;

Now user has two more chances if he/she fails.

What's next?

More control

You can implement more response methods to get more control.

  • OnTimeOutAsync
  • OnConversationErrorAsync
  • OnUnrelatedUpdateAsync
  • OnCancelAsync

God like control

In the most advanced use of package you can handle more complex conversations. As instance you may want to show a menu to the user using inline buttons and catch the response.

It's possible using UpdateCrackers.

Take a look at SurveyBot. Crackers are added in handler ( Program.cs )

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.1.0-preview.0.1 143 3/8/2022
1.0.4-alpha 114 2/21/2022
1.0.2-alpha 112 2/21/2022
1.0.1-alpha 113 2/20/2022