diff --git a/Examples/AsyncFormUpdates/App.config b/Examples/AsyncFormUpdates/App.config new file mode 100644 index 0000000..56efbc7 --- /dev/null +++ b/Examples/AsyncFormUpdates/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Examples/AsyncFormUpdates/AsyncFormUpdates.csproj b/Examples/AsyncFormUpdates/AsyncFormUpdates.csproj new file mode 100644 index 0000000..212f87e --- /dev/null +++ b/Examples/AsyncFormUpdates/AsyncFormUpdates.csproj @@ -0,0 +1,69 @@ + + + + + Debug + AnyCPU + {673A56F5-6110-4AED-A68D-562FD6ED3EA6} + Exe + AsyncFormUpdates + AsyncFormUpdates + v4.7.2 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll + + + + + + + + + + + ..\..\packages\Telegram.Bot.15.7.1\lib\net45\Telegram.Bot.dll + + + + + + + + + + + + + + + + {0bd16fb9-7ed4-4ccb-83eb-5cee538e1b6c} + TelegramBotBase + + + + \ No newline at end of file diff --git a/Examples/AsyncFormUpdates/Program.cs b/Examples/AsyncFormUpdates/Program.cs new file mode 100644 index 0000000..f249391 --- /dev/null +++ b/Examples/AsyncFormUpdates/Program.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Timers; + +namespace AsyncFormUpdates +{ + class Program + { + static TelegramBotBase.BotBase bot = null; + + static void Main(string[] args) + { + String apiKey = "APIKey"; + + bot = new TelegramBotBase.BotBase(apiKey); + + bot.Start(); + + var timer = new Timer(5000); + + timer.Elapsed += Timer_Elapsed; + timer.Start(); + + Console.ReadLine(); + + timer.Stop(); + bot.Stop(); + } + + private static async void Timer_Elapsed(object sender, ElapsedEventArgs e) + { + + foreach(var s in bot.Sessions.SessionList) + { + //Only for AsyncUpdateForm + if (s.Value.ActiveForm.GetType() != typeof(forms.AsyncFormUpdate) && s.Value.ActiveForm.GetType() != typeof(forms.AsyncFormEdit)) + continue; + + await bot.InvokeMessageLoop(s.Key); + } + + + } + } +} diff --git a/Examples/AsyncFormUpdates/Properties/AssemblyInfo.cs b/Examples/AsyncFormUpdates/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..216bef4 --- /dev/null +++ b/Examples/AsyncFormUpdates/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// Allgemeine Informationen über eine Assembly werden über die folgenden +// Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern, +// die einer Assembly zugeordnet sind. +[assembly: AssemblyTitle("AsyncFormUpdates")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("AsyncFormUpdates")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Durch Festlegen von ComVisible auf FALSE werden die Typen in dieser Assembly +// für COM-Komponenten unsichtbar. Wenn Sie auf einen Typ in dieser Assembly von +// COM aus zugreifen müssen, sollten Sie das ComVisible-Attribut für diesen Typ auf "True" festlegen. +[assembly: ComVisible(false)] + +// Die folgende GUID bestimmt die ID der Typbibliothek, wenn dieses Projekt für COM verfügbar gemacht wird +[assembly: Guid("673a56f5-6110-4aed-a68d-562fd6ed3ea6")] + +// Versionsinformationen für eine Assembly bestehen aus den folgenden vier Werten: +// +// Hauptversion +// Nebenversion +// Buildnummer +// Revision +// +// Sie können alle Werte angeben oder Standardwerte für die Build- und Revisionsnummern verwenden, +// indem Sie "*" wie unten gezeigt eingeben: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Examples/AsyncFormUpdates/forms/AsyncFormEdit.cs b/Examples/AsyncFormUpdates/forms/AsyncFormEdit.cs new file mode 100644 index 0000000..1e176d7 --- /dev/null +++ b/Examples/AsyncFormUpdates/forms/AsyncFormEdit.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TelegramBotBase.Attributes; +using TelegramBotBase.Base; +using TelegramBotBase.Form; + +namespace AsyncFormUpdates.forms +{ + public class AsyncFormEdit : FormBase + { + [SaveState] + int counter = 0; + + int MessageId = 0; + + public override async Task Load(MessageResult message) + { + counter++; + } + + public override async Task Action(MessageResult message) + { + await message.ConfirmAction(""); + + switch (message.RawData ?? "") + { + case "back": + + var st = new Start(); + await NavigateTo(st); + + break; + } + } + + public override async Task Render(MessageResult message) + { + var bf = new ButtonForm(); + bf.AddButtonRow("Back", "back"); + + if (MessageId != 0) + { + await Device.Edit(MessageId, $"Your current count is at: {counter}", bf); + } + else + { + var m = await Device.Send($"Your current count is at: {counter}", bf, disableNotification: true); + MessageId = m.MessageId; + } + + } + + } +} diff --git a/Examples/AsyncFormUpdates/forms/AsyncFormUpdate.cs b/Examples/AsyncFormUpdates/forms/AsyncFormUpdate.cs new file mode 100644 index 0000000..4a4b4c7 --- /dev/null +++ b/Examples/AsyncFormUpdates/forms/AsyncFormUpdate.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TelegramBotBase.Attributes; +using TelegramBotBase.Base; +using TelegramBotBase.Form; + +namespace AsyncFormUpdates.forms +{ + public class AsyncFormUpdate : AutoCleanForm + { + [SaveState] + int counter = 0; + + + public override async Task Load(MessageResult message) + { + counter++; + } + + public override async Task Action(MessageResult message) + { + await message.ConfirmAction(""); + + switch (message.RawData ?? "") + { + case "back": + + var st = new Start(); + await NavigateTo(st); + + break; + } + } + + public override async Task Render(MessageResult message) + { + var bf = new ButtonForm(); + bf.AddButtonRow("Back", "back"); + + await Device.Send($"Your current count is at: {counter}", bf, disableNotification: true); + } + + + } +} diff --git a/Examples/AsyncFormUpdates/forms/Start.cs b/Examples/AsyncFormUpdates/forms/Start.cs new file mode 100644 index 0000000..c69fa4a --- /dev/null +++ b/Examples/AsyncFormUpdates/forms/Start.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TelegramBotBase.Base; +using TelegramBotBase.Form; + +namespace AsyncFormUpdates.forms +{ + public class Start : AutoCleanForm + { + + + public override async Task Action(MessageResult message) + { + await message.ConfirmAction(""); + + switch (message.RawData ?? "") + { + case "async": + + var afe = new AsyncFormEdit(); + await NavigateTo(afe); + + + break; + + case "async_del": + + var afu = new AsyncFormUpdate(); + await NavigateTo(afu); + + + break; + } + + } + + public override async Task Render(MessageResult message) + { + var bf = new ButtonForm(); + + bf.AddButtonRow("Open Async Form with AutoCleanupForm", "async_del"); + + bf.AddButtonRow("Open Async Form with Edit", "async"); + + await Device.Send("Choose your option", bf); + } + + } +} diff --git a/Examples/AsyncFormUpdates/packages.config b/Examples/AsyncFormUpdates/packages.config new file mode 100644 index 0000000..e25dbb0 --- /dev/null +++ b/Examples/AsyncFormUpdates/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 069cbec..11e605d 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,8 @@ Thanks ! * [CheckedButtonList](#checked-button-list) + * [MultiToggleButton](#multi-toggle-button) + - [Groups](#groups) * [SplitterForm](#splitter-form) @@ -814,6 +816,8 @@ await this.NavigateTo(cd); ### Checked Button List +### Multi Toggle Button + ## Groups @@ -896,6 +900,7 @@ public class GroupForm : FormBase case Telegram.Bot.Types.Enums.MessageType.MessagePinned: case Telegram.Bot.Types.Enums.MessageType.GroupCreated: case Telegram.Bot.Types.Enums.MessageType.SupergroupCreated: + case Telegram.Bot.Types.Enums.MessageType.ChannelCreated: await OnGroupChanged(new GroupChangedEventArgs(message.MessageType, message)); @@ -1081,6 +1086,9 @@ Will allow you to run specific system commands or run/kill processes via Bot. Ha Will delete Join and Leave messages automatically in groups. +- [Examples/AsyncFormUpdates/](Examples/AsyncFormUpdates/) + +When you want to update forms async without any user interaction (message/action) before. Use the new InvokeMessageLoop method of BotBase. --- diff --git a/TelegramBotBase/Args/ButtonClickedEventArgs.cs b/TelegramBotBase/Args/ButtonClickedEventArgs.cs index c3fe2db..a99b9f8 100644 --- a/TelegramBotBase/Args/ButtonClickedEventArgs.cs +++ b/TelegramBotBase/Args/ButtonClickedEventArgs.cs @@ -16,6 +16,8 @@ namespace TelegramBotBase.Args public int Index { get; set; } + public object Tag { get; set; } + public ButtonClickedEventArgs() { diff --git a/TelegramBotBase/Args/MessageDeletedEventArgs.cs b/TelegramBotBase/Args/MessageDeletedEventArgs.cs new file mode 100644 index 0000000..69d262b --- /dev/null +++ b/TelegramBotBase/Args/MessageDeletedEventArgs.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Telegram.Bot.Types; + +namespace TelegramBotBase.Args +{ + public class MessageDeletedEventArgs + { + public int MessageId + { + get;set; + } + + public MessageDeletedEventArgs(int messageId) + { + this.MessageId = messageId; + } + + } +} diff --git a/TelegramBotBase/Args/PromptDialogCompletedEventArgs.cs b/TelegramBotBase/Args/PromptDialogCompletedEventArgs.cs new file mode 100644 index 0000000..14f9cf8 --- /dev/null +++ b/TelegramBotBase/Args/PromptDialogCompletedEventArgs.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace TelegramBotBase.Args +{ + public class PromptDialogCompletedEventArgs + { + public object Tag { get; set; } + + public String Value { get; set; } + + } +} diff --git a/TelegramBotBase/Base/MessageResult.cs b/TelegramBotBase/Base/MessageResult.cs index b7099c7..7da2bdd 100644 --- a/TelegramBotBase/Base/MessageResult.cs +++ b/TelegramBotBase/Base/MessageResult.cs @@ -20,12 +20,18 @@ namespace TelegramBotBase.Base { get { - return this.RawMessageData?.Message?.Chat.Id ?? this.RawCallbackData?.CallbackQuery.Message?.Chat.Id ?? 0; + return this.RawMessageData?.Message?.Chat.Id + ?? this.RawCallbackData?.CallbackQuery.Message?.Chat.Id + ?? Device?.DeviceId + ?? 0; } } public DeviceSession Device - { get; set; } + { + get; + set; + } /// /// The message id @@ -34,7 +40,9 @@ namespace TelegramBotBase.Base { get { - return this.Message?.MessageId ?? this.RawCallbackData?.CallbackQuery?.Message?.MessageId ?? 0; + return this.Message?.MessageId + ?? this.RawCallbackData?.CallbackQuery?.Message?.MessageId + ?? 0; } } @@ -58,7 +66,8 @@ namespace TelegramBotBase.Base { get { - return this.RawMessageData?.Message?.Type ?? Telegram.Bot.Types.Enums.MessageType.Unknown; + return this.RawMessageData?.Message?.Type + ?? Telegram.Bot.Types.Enums.MessageType.Unknown; } } @@ -175,6 +184,10 @@ namespace TelegramBotBase.Base } } + internal MessageResult() + { + + } public MessageResult(Telegram.Bot.Args.MessageEventArgs rawdata) { diff --git a/TelegramBotBase/BotBase.cs b/TelegramBotBase/BotBase.cs index 8176234..9d9c354 100644 --- a/TelegramBotBase/BotBase.cs +++ b/TelegramBotBase/BotBase.cs @@ -72,6 +72,7 @@ namespace TelegramBotBase { this.SystemSettings = new Dictionary(); + SetSetting(eSettings.MaxNumberOfRetries, 5); SetSetting(eSettings.NavigationMaximum, 10); SetSetting(eSettings.LogAllMessages, false); SetSetting(eSettings.SkipAllMessages, false); @@ -177,6 +178,8 @@ namespace TelegramBotBase }); } + DeviceSession.MaxNumberOfRetries = this.GetSetting(eSettings.MaxNumberOfRetries, 5); + this.Client.TelegramClient.StartReceiving(); } @@ -230,7 +233,7 @@ namespace TelegramBotBase ds?.OnMessageReceived(new MessageReceivedEventArgs(e.Message)); - await Client_TryMessage(sender, e); + await Client_Loop(sender, e); } catch (Telegram.Bot.Exceptions.ApiRequestException ex) { @@ -243,14 +246,86 @@ namespace TelegramBotBase } } - private async Task Client_TryMessage(object sender, MessageResult e) + + + //private async Task Client_TryMessage(object sender, MessageResult e) + //{ + // DeviceSession ds = e.Device; + // if (ds == null) + // { + // ds = await this.Sessions.StartSession(e.DeviceId); + // e.Device = ds; + + // ds.LastMessage = e.Message; + + // OnSessionBegins(new SessionBeginEventArgs(e.DeviceId, ds)); + // } + + // ds.LastAction = DateTime.Now; + // ds.LastMessage = e.Message; + + // //Is this a bot command ? + // if (e.IsBotCommand && this.BotCommands.Count(a => "/" + a.Command == e.BotCommand) > 0) + // { + // var sce = new BotCommandEventArgs(e.BotCommand, e.BotCommandParameters, e.Message, ds.DeviceId, ds); + // await OnBotCommand(sce); + + // if (sce.Handled) + // return; + // } + + // FormBase activeForm = null; + + // int i = 0; + + // //Should formulars get navigated (allow maximum of 10, to dont get loops) + // do + // { + // i++; + + // //Reset navigation + // ds.FormSwitched = false; + + // activeForm = ds.ActiveForm; + + // //Pre Loading Event + // await activeForm.PreLoad(e); + + // //Send Load event to controls + // await activeForm.LoadControls(e); + + // //Loading Event + // await activeForm.Load(e); + + // //Is Attachment ? (Photo, Audio, Video, Contact, Location, Document) + // if (e.Message.Type == Telegram.Bot.Types.Enums.MessageType.Contact | e.Message.Type == Telegram.Bot.Types.Enums.MessageType.Document | e.Message.Type == Telegram.Bot.Types.Enums.MessageType.Location | + // e.Message.Type == Telegram.Bot.Types.Enums.MessageType.Photo | e.Message.Type == Telegram.Bot.Types.Enums.MessageType.Video | e.Message.Type == Telegram.Bot.Types.Enums.MessageType.Audio) + // { + // await activeForm.SentData(new DataResult(e)); + // } + + // //Render Event + // if (!ds.FormSwitched) + // { + // await activeForm.RenderControls(e); + + // await activeForm.Render(e); + // } + + // e.IsFirstHandler = false; + + // } while (ds.FormSwitched && i < this.GetSetting(eSettings.NavigationMaximum, 10)); + + + //} + + private async Task Client_Loop(object sender, MessageResult e) { DeviceSession ds = e.Device; if (ds == null) { ds = await this.Sessions.StartSession(e.DeviceId); e.Device = ds; - ds.LastMessage = e.Message; OnSessionBegins(new SessionBeginEventArgs(e.DeviceId, ds)); @@ -293,15 +368,41 @@ namespace TelegramBotBase await activeForm.Load(e); //Is Attachment ? (Photo, Audio, Video, Contact, Location, Document) - if (e.Message.Type == Telegram.Bot.Types.Enums.MessageType.Contact | e.Message.Type == Telegram.Bot.Types.Enums.MessageType.Document | e.Message.Type == Telegram.Bot.Types.Enums.MessageType.Location | - e.Message.Type == Telegram.Bot.Types.Enums.MessageType.Photo | e.Message.Type == Telegram.Bot.Types.Enums.MessageType.Video | e.Message.Type == Telegram.Bot.Types.Enums.MessageType.Audio) + if (e.MessageType == Telegram.Bot.Types.Enums.MessageType.Contact | e.MessageType == Telegram.Bot.Types.Enums.MessageType.Document | e.MessageType == Telegram.Bot.Types.Enums.MessageType.Location | + e.MessageType == Telegram.Bot.Types.Enums.MessageType.Photo | e.MessageType == Telegram.Bot.Types.Enums.MessageType.Video | e.MessageType == Telegram.Bot.Types.Enums.MessageType.Audio) { await activeForm.SentData(new DataResult(e)); } - //Render Event + //Action Event + if (!ds.FormSwitched && e.IsAction) + { + //Send Action event to controls + await activeForm.ActionControls(e); + + //Send Action event to form itself + await activeForm.Action(e); + + if (!e.Handled) + { + var uhc = new UnhandledCallEventArgs(e.Message.Text, e.RawData, ds.DeviceId, e.MessageId, e.Message, ds); + OnUnhandledCall(uhc); + + if (uhc.Handled) + { + e.Handled = true; + if (!ds.FormSwitched) + { + break; + } + } + } + + } + if (!ds.FormSwitched) { + //Render Event await activeForm.RenderControls(e); await activeForm.Render(e); @@ -314,6 +415,38 @@ namespace TelegramBotBase } + /// + /// This will invoke the full message loop for the device even when no "userevent" like message or action has been raised. + /// + /// Contains the device/chat id of the device to update. + public async Task InvokeMessageLoop(long DeviceId) + { + var mr = new MessageResult(); + + await InvokeMessageLoop(DeviceId, mr); + } + + /// + /// This will invoke the full message loop for the device even when no "userevent" like message or action has been raised. + /// + /// Contains the device/chat id of the device to update. + /// + public async Task InvokeMessageLoop(long DeviceId, MessageResult e) + { + try + { + DeviceSession ds = this.Sessions.GetSession(DeviceId); + e.Device = ds; + + await Client_Loop(this, e); + } + catch (Exception ex) + { + DeviceSession ds = this.Sessions.GetSession(DeviceId); + OnException(new SystemExceptionEventArgs(e.Message.Text, DeviceId, ds, ex)); + } + } + private async void Client_MessageEdit(object sender, MessageResult e) { if (this.GetSetting(eSettings.SkipAllMessages, false)) @@ -363,12 +496,12 @@ namespace TelegramBotBase //When form has been switched due navigation within the edit method, reopen Client_Message if (ds.FormSwitched) { - await Client_TryMessage(sender, e); + await Client_Loop(sender, e); } } - private void Client_Action(object sender, MessageResult e) + private async void Client_Action(object sender, MessageResult e) { try { @@ -380,7 +513,7 @@ namespace TelegramBotBase OnMessage(new MessageIncomeEventArgs(e.DeviceId, ds, e)); } - Client_TryAction(sender, e); + await Client_Loop(sender, e); } catch (Exception ex) { @@ -389,80 +522,80 @@ namespace TelegramBotBase } } - private async void Client_TryAction(object sender, MessageResult e) - { - DeviceSession ds = e.Device; - if (ds == null) - { - ds = await this.Sessions.StartSession(e.DeviceId); - e.Device = ds; - } + //private async void Client_TryAction(object sender, MessageResult e) + //{ + // DeviceSession ds = e.Device; + // if (ds == null) + // { + // ds = await this.Sessions.StartSession(e.DeviceId); + // e.Device = ds; + // } - ds.LastAction = DateTime.Now; - ds.LastMessage = e.Message; + // ds.LastAction = DateTime.Now; + // ds.LastMessage = e.Message; - FormBase activeForm = null; + // FormBase activeForm = null; - int i = 0; + // int i = 0; - //Should formulars get navigated (allow maximum of 10, to dont get loops) - do - { - i++; + // //Should formulars get navigated (allow maximum of 10, to dont get loops) + // do + // { + // i++; - //Reset navigation - ds.FormSwitched = false; + // //Reset navigation + // ds.FormSwitched = false; - activeForm = ds.ActiveForm; + // activeForm = ds.ActiveForm; - //Pre Loading Event - await activeForm.PreLoad(e); + // //Pre Loading Event + // await activeForm.PreLoad(e); - //Send Load event to controls - await activeForm.LoadControls(e); + // //Send Load event to controls + // await activeForm.LoadControls(e); - //Loading Event - await activeForm.Load(e); + // //Loading Event + // await activeForm.Load(e); - //Action Event - if (!ds.FormSwitched) - { - //Send Action event to controls - await activeForm.ActionControls(e); + // //Action Event + // if (!ds.FormSwitched) + // { + // //Send Action event to controls + // await activeForm.ActionControls(e); - //Send Action event to form itself - await activeForm.Action(e); + // //Send Action event to form itself + // await activeForm.Action(e); - if (!e.Handled) - { - var uhc = new UnhandledCallEventArgs(e.Message.Text, e.RawData, ds.DeviceId, e.MessageId, e.Message, ds); - OnUnhandledCall(uhc); + // if (!e.Handled) + // { + // var uhc = new UnhandledCallEventArgs(e.Message.Text, e.RawData, ds.DeviceId, e.MessageId, e.Message, ds); + // OnUnhandledCall(uhc); - if (uhc.Handled) - { - e.Handled = true; - if (!ds.FormSwitched) - { - break; - } - } - } + // if (uhc.Handled) + // { + // e.Handled = true; + // if (!ds.FormSwitched) + // { + // break; + // } + // } + // } - } + // } - //Render Event - if (!ds.FormSwitched) - { - await activeForm.RenderControls(e); + // //Render Event + // if (!ds.FormSwitched) + // { + // await activeForm.RenderControls(e); - await activeForm.Render(e); - } + // await activeForm.Render(e); + // } - e.IsFirstHandler = false; + // e.IsFirstHandler = false; - } while (ds.FormSwitched && i < this.GetSetting(eSettings.NavigationMaximum, 10)); + // } while (ds.FormSwitched && i < this.GetSetting(eSettings.NavigationMaximum, 10)); - } + //} /// /// This method will update all local created bot commands to the botfather. diff --git a/TelegramBotBase/Commands/Extensions.cs b/TelegramBotBase/Commands/Extensions.cs new file mode 100644 index 0000000..b866439 --- /dev/null +++ b/TelegramBotBase/Commands/Extensions.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Telegram.Bot.Types; + +namespace TelegramBotBase.Commands +{ + public static class Extensions + { + /// + /// Adding the default /start command with a description. + /// + /// + /// + public static void AddStartCommand(this List cmds, String description) + { + cmds.Add(new BotCommand() { Command = "start", Description = description }); + } + + /// + /// Adding the default /help command with a description. + /// + /// + /// + public static void AddHelpCommand(this List cmds, String description) + { + cmds.Add(new BotCommand() { Command = "help", Description = description }); + } + + /// + /// Adding the default /settings command with a description. + /// + /// + /// + public static void AddSettingsCommand(this List cmds, String description) + { + cmds.Add(new BotCommand() { Command = "settings", Description = description }); + } + } +} diff --git a/TelegramBotBase/Constants/Telegram.cs b/TelegramBotBase/Constants/Telegram.cs index b7392ee..d203123 100644 --- a/TelegramBotBase/Constants/Telegram.cs +++ b/TelegramBotBase/Constants/Telegram.cs @@ -21,5 +21,7 @@ namespace TelegramBotBase.Constants public const int MaxReplyKeyboardCols = 12; + public const int MessageDeletionsPerSecond = 30; + } } diff --git a/TelegramBotBase/Controls/Hybrid/ButtonGrid.cs b/TelegramBotBase/Controls/Hybrid/ButtonGrid.cs index 99c96b4..7a1318b 100644 --- a/TelegramBotBase/Controls/Hybrid/ButtonGrid.cs +++ b/TelegramBotBase/Controls/Hybrid/ButtonGrid.cs @@ -159,6 +159,22 @@ namespace TelegramBotBase.Controls.Hybrid } } + public override void Init() + { + this.Device.MessageDeleted += Device_MessageDeleted; + } + + private void Device_MessageDeleted(object sender, MessageDeletedEventArgs e) + { + if (this.MessageId == null) + return; + + if (e.MessageId != this.MessageId) + return; + + this.MessageId = null; + } + public async override Task Load(MessageResult result) { if (this.KeyboardType != eKeyboardType.ReplyKeyboard) @@ -396,14 +412,20 @@ namespace TelegramBotBase.Controls.Hybrid case eKeyboardType.InlineKeyBoard: + //Try to edit message if message id is available + //When the returned message is null then the message has been already deleted, resend it if (this.MessageId != null) { m = await this.Device.Edit(this.MessageId.Value, this.Title, (InlineKeyboardMarkup)form); + if (m != null) + { + this.MessageId = m.MessageId; + return; + } } - else - { - m = await this.Device.Send(this.Title, (InlineKeyboardMarkup)form, disableNotification: true, parseMode: MessageParseMode, MarkdownV2AutoEscape: false); - } + + //When no message id is available or it has been deleted due the use of AutoCleanForm re-render automatically + m = await this.Device.Send(this.Title, (InlineKeyboardMarkup)form, disableNotification: true, parseMode: MessageParseMode, MarkdownV2AutoEscape: false); break; } @@ -573,6 +595,11 @@ namespace TelegramBotBase.Controls.Hybrid { this.Updated(); } + else + { + //Remove event handler + this.Device.MessageDeleted -= Device_MessageDeleted; + } } /// diff --git a/TelegramBotBase/Controls/Hybrid/TaggedButtonGrid.cs b/TelegramBotBase/Controls/Hybrid/TaggedButtonGrid.cs index 9fb8d5f..31028e9 100644 --- a/TelegramBotBase/Controls/Hybrid/TaggedButtonGrid.cs +++ b/TelegramBotBase/Controls/Hybrid/TaggedButtonGrid.cs @@ -170,6 +170,22 @@ namespace TelegramBotBase.Controls.Hybrid } } + public override void Init() + { + this.Device.MessageDeleted += Device_MessageDeleted; + } + + private void Device_MessageDeleted(object sender, MessageDeletedEventArgs e) + { + if (this.MessageId == null) + return; + + if (e.MessageId != this.MessageId) + return; + + this.MessageId = null; + } + public async override Task Load(MessageResult result) { if (this.KeyboardType != eKeyboardType.ReplyKeyboard) @@ -185,10 +201,7 @@ namespace TelegramBotBase.Controls.Hybrid ?? SubHeadLayoutButtonRow?.FirstOrDefault(a => a.Text.Trim() == result.MessageText) ?? ButtonsForm.ToList().FirstOrDefault(a => a.Text.Trim() == result.MessageText); - var index = HeadLayoutButtonRow?.IndexOf(button) - ?? SubHeadLayoutButtonRow?.IndexOf(button) - ?? ButtonsForm.ToList().IndexOf(button); - + var index = ButtonsForm.FindRowByButton(button); switch (this.SelectedViewIndex) @@ -326,9 +339,7 @@ namespace TelegramBotBase.Controls.Hybrid ?? SubHeadLayoutButtonRow?.FirstOrDefault(a => a.Value == result.RawData) ?? ButtonsForm.ToList().FirstOrDefault(a => a.Value == result.RawData); - var index = HeadLayoutButtonRow?.IndexOf(button) - ?? SubHeadLayoutButtonRow?.IndexOf(button) - ?? ButtonsForm.ToList().IndexOf(button); + var index = ButtonsForm.FindRowByButton(button); if (button != null) { @@ -521,15 +532,21 @@ namespace TelegramBotBase.Controls.Hybrid case eKeyboardType.InlineKeyBoard: + + //Try to edit message if message id is available + //When the returned message is null then the message has been already deleted, resend it if (this.MessageId != null) { m = await this.Device.Edit(this.MessageId.Value, this.Title, (InlineKeyboardMarkup)form); - } - else - { - m = await this.Device.Send(this.Title, (InlineKeyboardMarkup)form, disableNotification: true, parseMode: MessageParseMode, MarkdownV2AutoEscape: false); + if (m != null) + { + this.MessageId = m.MessageId; + return; + } } + //When no message id is available or it has been deleted due the use of AutoCleanForm re-render automatically + m = await this.Device.Send(this.Title, (InlineKeyboardMarkup)form, disableNotification: true, parseMode: MessageParseMode, MarkdownV2AutoEscape: false); break; } @@ -782,6 +799,11 @@ namespace TelegramBotBase.Controls.Hybrid { this.Updated(); } + else + { + //Remove event handler + this.Device.MessageDeleted -= Device_MessageDeleted; + } } /// diff --git a/TelegramBotBase/Controls/Inline/MultiToggleButton.cs b/TelegramBotBase/Controls/Inline/MultiToggleButton.cs new file mode 100644 index 0000000..1828f2b --- /dev/null +++ b/TelegramBotBase/Controls/Inline/MultiToggleButton.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TelegramBotBase.Base; +using TelegramBotBase.Form; + +namespace TelegramBotBase.Controls.Inline +{ + public class MultiToggleButton : ControlBase + { + /// + /// This contains the selected icon. + /// + public String SelectedIcon { get; set; } = Localizations.Default.Language["MultiToggleButton_SelectedIcon"]; + + /// + /// This will appear on the ConfirmAction message (if not empty) + /// + public String ChangedString { get; set; } = Localizations.Default.Language["MultiToggleButton_Changed"]; + + /// + /// This holds the title of the control. + /// + public String Title { get; set; } = Localizations.Default.Language["MultiToggleButton_Title"]; + + public int? MessageId { get; set; } + + private bool RenderNecessary = true; + + private static readonly object __evToggled = new object(); + + private readonly EventHandlerList Events = new EventHandlerList(); + + /// + /// This will hold all options available. + /// + public List Options { get; set; } + + /// + /// This will set if an empty selection (null) is allowed. + /// + public bool AllowEmptySelection { get; set; } = true; + + + public MultiToggleButton() + { + Options = new List(); + } + + public event EventHandler Toggled + { + add + { + this.Events.AddHandler(__evToggled, value); + } + remove + { + this.Events.RemoveHandler(__evToggled, value); + } + } + + public void OnToggled(EventArgs e) + { + (this.Events[__evToggled] as EventHandler)?.Invoke(this, e); + } + + public override async Task Action(MessageResult result, String value = null) + { + if (result.Handled) + return; + + await result.ConfirmAction(this.ChangedString); + + switch (value ?? "unknown") + { + default: + + var s = value.Split('$'); + + if (s[0] == "check" && s.Length > 1) + { + int index = 0; + if (!int.TryParse(s[1], out index)) + { + return; + } + + if(SelectedOption== null || SelectedOption != this.Options[index]) + { + this.SelectedOption = this.Options[index]; + OnToggled(new EventArgs()); + } + else if(this.AllowEmptySelection) + { + this.SelectedOption = null; + OnToggled(new EventArgs()); + } + + RenderNecessary = true; + + return; + } + + + RenderNecessary = false; + + break; + + } + + result.Handled = true; + + } + + public override async Task Render(MessageResult result) + { + if (!RenderNecessary) + return; + + var bf = new ButtonForm(this); + + var lst = new List(); + foreach (var o in this.Options) + { + var index = this.Options.IndexOf(o); + if (o == this.SelectedOption) + { + lst.Add(new ButtonBase(SelectedIcon + " " + o.Text, "check$" + index)); + continue; + } + + lst.Add(new ButtonBase(o.Text, "check$" + index)); + } + + bf.AddButtonRow(lst); + + if (this.MessageId != null) + { + var m = await this.Device.Edit(this.MessageId.Value, this.Title, bf); + } + else + { + var m = await this.Device.Send(this.Title, bf, disableNotification: true); + if (m != null) + { + this.MessageId = m.MessageId; + } + } + + this.RenderNecessary = false; + + + } + + public ButtonBase SelectedOption + { + get; set; + } + + } +} diff --git a/TelegramBotBase/Enums/eSettings.cs b/TelegramBotBase/Enums/eSettings.cs index e425650..7e55710 100644 --- a/TelegramBotBase/Enums/eSettings.cs +++ b/TelegramBotBase/Enums/eSettings.cs @@ -27,9 +27,14 @@ namespace TelegramBotBase.Enums /// /// Does stick to the console event handler and saves all sessions on exit. /// - SaveSessionsOnConsoleExit = 4 + SaveSessionsOnConsoleExit = 4, + /// + /// Indicates the maximum number of times a request that received error + /// 429 will be sent again after a timeout until it receives code 200 or an error code not equal to 429. + /// + MaxNumberOfRetries = 5, } } diff --git a/TelegramBotBase/Form/ArrayPromptDialog.cs b/TelegramBotBase/Form/ArrayPromptDialog.cs index 0812b25..42749fe 100644 --- a/TelegramBotBase/Form/ArrayPromptDialog.cs +++ b/TelegramBotBase/Form/ArrayPromptDialog.cs @@ -16,8 +16,16 @@ namespace TelegramBotBase.Form [IgnoreState] public class ArrayPromptDialog : FormBase { + /// + /// The message the users sees. + /// public String Message { get; set; } + /// + /// An additional optional value. + /// + public object Tag { get; set; } + public ButtonBase[][] Buttons { get; set; } [Obsolete] @@ -70,7 +78,7 @@ namespace TelegramBotBase.Form return; } - OnButtonClicked(new ButtonClickedEventArgs(button)); + OnButtonClicked(new ButtonClickedEventArgs(button) { Tag = this.Tag }); FormBase fb = ButtonForms.ContainsKey(call.Value) ? ButtonForms[call.Value] : null; diff --git a/TelegramBotBase/Form/AutoCleanForm.cs b/TelegramBotBase/Form/AutoCleanForm.cs index 20aa644..4a7c6c5 100644 --- a/TelegramBotBase/Form/AutoCleanForm.cs +++ b/TelegramBotBase/Form/AutoCleanForm.cs @@ -25,7 +25,7 @@ namespace TelegramBotBase.Form [SaveState] public eDeleteSide DeleteSide { get; set; } - + public AutoCleanForm() { @@ -33,13 +33,12 @@ namespace TelegramBotBase.Form this.DeleteMode = eDeleteMode.OnEveryCall; this.DeleteSide = eDeleteSide.BotOnly; - this.Init += AutoCleanForm_Init; + this.Init += AutoCleanForm_Init; this.Closed += AutoCleanForm_Closed; } - private async Task AutoCleanForm_Init(object sender, InitEventArgs e) { if (this.Device == null) @@ -130,13 +129,23 @@ namespace TelegramBotBase.Form { while (this.OldMessages.Count > 0) { - if (!await this.Device.DeleteMessage(this.OldMessages[0])) + var tasks = new List(); + var msgs = this.OldMessages.Take(Constants.Telegram.MessageDeletionsPerSecond); + + foreach (var msg in msgs) { - //Message can't be deleted cause it seems not to exist anymore - if (this.OldMessages.Count > 0) - this.OldMessages.RemoveAt(0); + tasks.Add(this.Device.DeleteMessage(msg)); } - } + + await Task.WhenAll(tasks); + + foreach(var m in msgs) + { + Device.OnMessageDeleted(new MessageDeletedEventArgs(m)); + } + + this.OldMessages.RemoveRange(0, msgs.Count()); + } } } } diff --git a/TelegramBotBase/Form/ConfirmDialog.cs b/TelegramBotBase/Form/ConfirmDialog.cs index a5ce824..dc791db 100644 --- a/TelegramBotBase/Form/ConfirmDialog.cs +++ b/TelegramBotBase/Form/ConfirmDialog.cs @@ -13,8 +13,16 @@ namespace TelegramBotBase.Form [IgnoreState] public class ConfirmDialog : ModalDialog { + /// + /// The message the users sees. + /// public String Message { get; set; } + /// + /// An additional optional value. + /// + public object Tag { get; set; } + /// /// Automatically close form on button click /// @@ -77,7 +85,7 @@ namespace TelegramBotBase.Form return; } - OnButtonClicked(new ButtonClickedEventArgs(button)); + OnButtonClicked(new ButtonClickedEventArgs(button) { Tag = this.Tag }); if (AutoCloseOnClick) await CloseForm(); diff --git a/TelegramBotBase/Form/GroupForm.cs b/TelegramBotBase/Form/GroupForm.cs index 3cfc949..6b460cf 100644 --- a/TelegramBotBase/Form/GroupForm.cs +++ b/TelegramBotBase/Form/GroupForm.cs @@ -41,6 +41,7 @@ namespace TelegramBotBase.Form case Telegram.Bot.Types.Enums.MessageType.MessagePinned: case Telegram.Bot.Types.Enums.MessageType.GroupCreated: case Telegram.Bot.Types.Enums.MessageType.SupergroupCreated: + case Telegram.Bot.Types.Enums.MessageType.ChannelCreated: await OnGroupChanged(new GroupChangedEventArgs(message.MessageType, message)); diff --git a/TelegramBotBase/Form/PromptDialog.cs b/TelegramBotBase/Form/PromptDialog.cs index f2a26cd..ec9e61c 100644 --- a/TelegramBotBase/Form/PromptDialog.cs +++ b/TelegramBotBase/Form/PromptDialog.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading.Tasks; using Telegram.Bot.Types; using Telegram.Bot.Types.ReplyMarkups; +using TelegramBotBase.Args; using TelegramBotBase.Attributes; using TelegramBotBase.Base; @@ -14,10 +15,21 @@ namespace TelegramBotBase.Form [IgnoreState] public class PromptDialog : ModalDialog { + /// + /// The message the users sees. + /// public String Message { get; set; } + /// + /// The returned text value by the user. + /// public String Value { get; set; } + /// + /// An additional optional value. + /// + public object Tag { get; set; } + private EventHandlerList __Events { get; set; } = new EventHandlerList(); private static object __evCompleted { get; } = new object(); @@ -86,13 +98,13 @@ namespace TelegramBotBase.Form message.Handled = true; - OnCompleted(new EventArgs()); + OnCompleted(new PromptDialogCompletedEventArgs() { Tag = this.Tag, Value = this.Value }); await this.CloseForm(); } - public event EventHandler Completed + public event EventHandler Completed { add { @@ -104,9 +116,9 @@ namespace TelegramBotBase.Form } } - public void OnCompleted(EventArgs e) + public void OnCompleted(PromptDialogCompletedEventArgs e) { - (this.__Events[__evCompleted] as EventHandler)?.Invoke(this, e); + (this.__Events[__evCompleted] as EventHandler)?.Invoke(this, e); } } diff --git a/TelegramBotBase/Localizations/English.cs b/TelegramBotBase/Localizations/English.cs index ed97089..1082c21 100644 --- a/TelegramBotBase/Localizations/English.cs +++ b/TelegramBotBase/Localizations/English.cs @@ -26,6 +26,10 @@ namespace TelegramBotBase.Localizations Values["ToggleButton_OnIcon"] = "⚫"; Values["ToggleButton_OffIcon"] = "⚪"; Values["ToggleButton_Title"] = "Toggle"; + Values["ToggleButton_Changed"] = "Choosen"; + Values["MultiToggleButton_SelectedIcon"] = "✅"; + Values["MultiToggleButton_Title"] = "Multi-Toggle"; + Values["MultiToggleButton_Changed"] = "Choosen"; Values["PromptDialog_Back"] = "Back"; Values["ToggleButton_Changed"] = "Setting changed"; } diff --git a/TelegramBotBase/Localizations/Localization.cs b/TelegramBotBase/Localizations/Localization.cs index e00c25c..7f60247 100644 --- a/TelegramBotBase/Localizations/Localization.cs +++ b/TelegramBotBase/Localizations/Localization.cs @@ -36,6 +36,10 @@ namespace TelegramBotBase.Localizations Values["ToggleButton_OnIcon"] = "⚫"; Values["ToggleButton_OffIcon"] = "⚪"; Values["ToggleButton_Title"] = "Schalter"; + Values["ToggleButton_Changed"] = "Ausgewählt"; + Values["MultiToggleButton_SelectedIcon"] = "✅"; + Values["MultiToggleButton_Title"] = "Mehrfach-Schalter"; + Values["MultiToggleButton_Changed"] = "Ausgewählt"; Values["PromptDialog_Back"] = "Zurück"; Values["ToggleButton_Changed"] = "Einstellung geändert"; diff --git a/TelegramBotBase/SessionBase.cs b/TelegramBotBase/SessionBase.cs index 67e309d..7096000 100644 --- a/TelegramBotBase/SessionBase.cs +++ b/TelegramBotBase/SessionBase.cs @@ -198,6 +198,8 @@ namespace TelegramBotBase catch (ArgumentException ex) { + CustomConversionChecks(form, p, f); + } catch { @@ -238,6 +240,35 @@ namespace TelegramBotBase } + private static void CustomConversionChecks(FormBase form, KeyValuePair p, System.Reflection.PropertyInfo f) + { + //Newtonsoft Int64/Int32 converter issue + if (f.PropertyType == typeof(Int32)) + { + int i = 0; + if(int.TryParse(p.Value.ToString(), out i)) + { + f.SetValue(form, i); + } + return; + } + + //Newtonsoft Double/Decimal converter issue + if(f.PropertyType == typeof(Decimal) | f.PropertyType == typeof(Nullable)) + { + decimal d = 0; + if(decimal.TryParse(p.Value.ToString(), out d)) + { + f.SetValue(form, d); + } + return; + } + + + } + + + /// /// Saves all open states into the machine. /// diff --git a/TelegramBotBase/Sessions/DeviceSession.cs b/TelegramBotBase/Sessions/DeviceSession.cs index 12d57d4..7ce93c1 100644 --- a/TelegramBotBase/Sessions/DeviceSession.cs +++ b/TelegramBotBase/Sessions/DeviceSession.cs @@ -119,6 +119,7 @@ namespace TelegramBotBase.Sessions private static object __evMessageSent = new object(); private static object __evMessageReceived = new object(); + private static object __evMessageDeleted = new object(); public DeviceSession() { @@ -769,20 +770,24 @@ namespace TelegramBotBase.Sessions /// public async Task API(Func> call) { - try + var numberOfTries = 0; + while (numberOfTries < DeviceSession.MaxNumberOfRetries) { - return await call(this.Client.TelegramClient); - } - catch (ApiRequestException ex) - { - if (ex.Parameters != null) + try { - await Task.Delay(ex.Parameters.RetryAfter); + return await call(Client.TelegramClient); + } + catch (ApiRequestException ex) + { + if (ex.ErrorCode != 429) + throw; - return await call(this.Client.TelegramClient); + if (ex.Parameters != null) + await Task.Delay(ex.Parameters.RetryAfter * 1000); + + numberOfTries++; } } - return default(T); } @@ -793,17 +798,23 @@ namespace TelegramBotBase.Sessions /// public async Task API(Func call) { - try + var numberOfTries = 0; + while (numberOfTries < DeviceSession.MaxNumberOfRetries) { - await call(this.Client.TelegramClient); - } - catch (ApiRequestException ex) - { - if (ex.Parameters != null) + try { - await Task.Delay(ex.Parameters.RetryAfter); + await call(Client.TelegramClient); + return; + } + catch (ApiRequestException ex) + { + if (ex.ErrorCode != 429) + throw; - await call(this.Client.TelegramClient); + if (ex.Parameters != null) + await Task.Delay(ex.Parameters.RetryAfter * 1000); + + numberOfTries++; } } } @@ -853,6 +864,37 @@ namespace TelegramBotBase.Sessions (this.__Events[__evMessageReceived] as EventHandler)?.Invoke(this, e); } + /// + /// Eventhandler for deleting messages + /// + public event EventHandler MessageDeleted + { + add + { + this.__Events.AddHandler(__evMessageDeleted, value); + } + remove + { + this.__Events.RemoveHandler(__evMessageDeleted, value); + } + } + + + public void OnMessageDeleted(MessageDeletedEventArgs e) + { + (this.__Events[__evMessageDeleted] as EventHandler)?.Invoke(this, e); + } + #endregion + + #region "Static" + + /// + /// Indicates the maximum number of times a request that received error + /// 429 will be sent again after a timeout until it receives code 200 or an error code not equal to 429. + /// + public static uint MaxNumberOfRetries { get; set; } + + #endregion "Static" } } diff --git a/TelegramBotBaseTest/Tests/Controls/MultiToggleButtons.cs b/TelegramBotBaseTest/Tests/Controls/MultiToggleButtons.cs new file mode 100644 index 0000000..b38abb4 --- /dev/null +++ b/TelegramBotBaseTest/Tests/Controls/MultiToggleButtons.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TelegramBotBase.Args; +using TelegramBotBase.Controls; +using TelegramBotBase.Controls.Inline; +using TelegramBotBase.Form; + +namespace TelegramBotBaseTest.Tests.Controls +{ + public class MultiToggleButtons : AutoCleanForm + { + public MultiToggleButtons() + { + this.DeleteMode = TelegramBotBase.Enums.eDeleteMode.OnLeavingForm; + + this.Init += ToggleButtons_Init; + } + + private async Task ToggleButtons_Init(object sender, InitEventArgs e) + { + + var mtb = new MultiToggleButton(); + + mtb.Options = new List() { new ButtonBase("Option 1", "1"), new ButtonBase("Option 2", "2"), new ButtonBase("Option 3", "3") }; + mtb.SelectedOption = mtb.Options.FirstOrDefault(); + mtb.Toggled += Tb_Toggled; + this.AddControl(mtb); + + mtb = new MultiToggleButton(); + + mtb.Options = new List() { new ButtonBase("Option 4", "4"), new ButtonBase("Option 5", "5"), new ButtonBase("Option 6", "6") }; + mtb.SelectedOption = mtb.Options.FirstOrDefault(); + mtb.AllowEmptySelection = false; + mtb.Toggled += Tb_Toggled; + this.AddControl(mtb); + } + + private void Tb_Toggled(object sender, EventArgs e) + { + var tb = sender as MultiToggleButton; + if (tb.SelectedOption != null) + { + Console.WriteLine(tb.ID.ToString() + " was pressed, and toggled to " + tb.SelectedOption.Value); + return; + } + + Console.WriteLine("Selection for " + tb.ID.ToString() + " has been removed."); + } + } +} diff --git a/TelegramBotBaseTest/Tests/Menu.cs b/TelegramBotBaseTest/Tests/Menu.cs index d72d04f..57cf9fb 100644 --- a/TelegramBotBaseTest/Tests/Menu.cs +++ b/TelegramBotBaseTest/Tests/Menu.cs @@ -140,6 +140,17 @@ namespace TelegramBotBaseTest.Tests await this.NavigateTo(tb); break; + + case "multitogglebuttons": + + message.Handled = true; + + var mtb = new Controls.MultiToggleButtons(); + + await this.NavigateTo(mtb); + + break; + case "buttongrid": message.Handled = true; @@ -217,6 +228,8 @@ namespace TelegramBotBaseTest.Tests btn.AddButtonRow(new ButtonBase("#11 ToggleButtons", new CallbackData("a", "togglebuttons").Serialize())); + btn.AddButtonRow(new ButtonBase("#11.2 MultiToggleButtons", new CallbackData("a", "multitogglebuttons").Serialize())); + btn.AddButtonRow(new ButtonBase("#12 ButtonGrid", new CallbackData("a", "buttongrid").Serialize())); btn.AddButtonRow(new ButtonBase("#13 ButtonGrid Paging & Filter", new CallbackData("a", "buttongridfilter").Serialize())); diff --git a/images/multitogglebutton.gif b/images/multitogglebutton.gif new file mode 100644 index 0000000..b399eae Binary files /dev/null and b/images/multitogglebutton.gif differ