How do I generate keystrokes in a non-form application - c#

So I have a huge program and decided I should make one of the methods run in a separate thread. So I put the method in a separate class, an activated it on my form. It seemed to worked just how I wanted it to until it got to part where it gave me this error:
SendKeys cannot run inside this application because the application
is not handling Windows messages. Either change the application to
handle messages, or use the SendKeys.SendWait method.
I tried looking for the answer online. I think I saw something about how SendKeys only works in a Form or something.
Can anyone tell me a way to simulate a keystroke without using SendKeys, OR a way to get SendKeys to work in a different, non-form thread?

Your console application needs a message loop. This is done through the Application class. You will need to call Application.Run(ApplicationContext).
class MyApplicationContext : ApplicationContext
{
[STAThread]
static void Main(string[] args)
{
// Create the MyApplicationContext, that derives from ApplicationContext,
// that manages when the application should exit.
MyApplicationContext context = new MyApplicationContext();
// Run the application with the specific context. It will exit when
// the task completes and calls Exit().
Application.Run(context);
}
Task backgroundTask;
// This is the constructor of the ApplicationContext, we do not want to
// block here.
private MyApplicationContext()
{
backgroundTask = Task.Factory.StartNew(BackgroundTask);
backgroundTask.ContinueWith(TaskComplete);
}
// This will allow the Application.Run(context) in the main function to
// unblock.
private void TaskComplete(Task src)
{
this.ExitThread();
}
//Perform your actual work here.
private void BackgroundTask()
{
//Stuff
SendKeys.Send("{RIGHT}");
//More stuff here
}
}

I Know this not an answer, but this how i used to do using ActiveX and Script
Set ws = CreateObject("WScript.Shell")
str = "Hi there... ~ Dont click your mouse while i am typing." & _
" ~~This is a send key example, using which you can send your keystrokes"
ws.Run("notepad.exe")
WScript.Sleep(1000)
For c=1 To Len(str)
WScript.Sleep(100) 'Increase the value for longer delay
ws.SendKeys Mid(str,c,1)
Next
Save this code as file.vbs and double click to execute in windows machine.

Related

WPF cannot close Application instance for running it a second time

I have an Console Application started as [STAThread].
That application should open a seperate Wpf UI for entering some settings.
The functions for that:
private static void openUI()
{
var application = new System.Windows.Application();
//referenced project in the same solution
var ui = new ManagerUI.MainWindow();
//blocks execution
application.Run(ui);
application.Shutdown();
}
Opening the UI for the first time works as expected.
The problem occurs when opening the UI for the second time.
I get an System.InvalidOperationException, saying that I cannot run more than one Application-Instance in the same AppDomain.
For saving ram, it must be closed between the operations.
I also tried to create the System.Windows.Application in the constructor.
But as soon as I run the application the second time, I get a very similiar exception.
The InitializeComponents() method of the UI throws an System.InvalidOperationException, saying that the Object is going to be terminated.
The StackTraces shows that the error appears when the xaml is parsed, so I conclude it cannot open it, because it is still opened by the first execution.
Neither calling ui.Close() nor calling application.Shutdown() solves the problem (Environment.Exit() closes everything, including my Console Application).
The ram profiler indicates, not everything was closed correctly because it shows an higher use after the Window was closed, than before it was opened in the firts place.
How do I properly close the Application instance, or how do I re-use it to run an Wpf Application multiple times?
Having looked at the source code for the Application class, it doesn't look like you will be able to work around this, as various static fields are initialized by the class constructor:
public Application()
{
...
lock(_globalLock)
{
if (_appCreatedInThisAppDomain == false)
{
...
_appInstance = this;
...
_appCreatedInThisAppDomain = true;
}
else
{
throw new InvalidOperationException(...);
}
}
}
...
static private object _globalLock;
static private bool _appCreatedInThisAppDomain;
static private Application _appInstance;
...
Basically the constructor sets _appCreatedInThisAppDomain to true, and as that field is private you have no way of setting it back*.
I think the only way of achieving something similar to what you want is to write a separate WPF application, then use the Process class to launch that from your console application. Alternatively, you could theoretically create a separate AppDomain to host your WPF stuff but that would be a lot more complicated.
[*] other than using Reflection, but let's not go there!
You may create a class that derives from MarshalByRefObject:
public class AppDomainWrapper : MarshalByRefObject
{
public void openUI()
{
var application = new System.Windows.Application();
var ui = new Window();
application.Run(ui);
application.Shutdown();
}
}
...and execute its openUI() method in its own application domain:
[STAThread]
static void Main(string[] args)
{
const int n = 2;
for (int i = 0; i < n; ++i)
{
AppDomain appDomain = AppDomain.CreateDomain("AppDomain");
AppDomainWrapper application = appDomain.CreateInstanceAndUnwrap(typeof(AppDomainWrapper).Assembly.FullName, typeof(AppDomainWrapper).FullName) as AppDomainWrapper;
application.openUI();
AppDomain.Unload(appDomain);
}
}
Have a look at this question:Does a WPF Application Actually Need Application.Run?.
Basically it says, that you can open windows using window.ShowDialog() method without Application instance
The think is that Application.Run does not do anything important but run Dispatcher loop. ShowDialog have its own Dispatcher. You can create Application singleton instance however, since it contains some shared resources.
Hack(run it after application.Shutdown()). I use this in tests:
var field = typeof(Application).GetField(
"_appCreatedInThisAppDomain",
BindingFlags.Static | BindingFlags.NonPublic) ??
throw new InvalidOperationException(
"Field is not found: _appCreatedInThisAppDomain.");
field.SetValue(null, false);
Steven Rands shows the problem.
I have the same problem in an external add-in. But I need an application object for xaml resources and a valid Application.Current.
In my eyes this is a bug. If you call Shutdown() this member should also be reset to false.

How to change NotifyIcon.Text in a running WinForm via command-line-arguments?

This is a tray-icon-only Windows Forms application. I'm trying to use argument to control something and change the text on the form for showing the status information.
But I found when I use argument to call it during it's running, the things I want to change are null (NotifyIcon() and MenuItem()), seems it ran a different application when I using arguments. I also tried Invoke() but there is no this definition in NotifyIcon().
Here is the code I wrote:
static void Main(string[] args)
{
if (args.Length > 0)
{
Arg_Call(args[0]);
}
if (new Mutex(true, "{XXX}").WaitOne(TimeSpan.Zero, true))
{
Init_Tray();
Application.Run();
}
}
private static NotifyIcon trayicon;
private static void Init_Tray()
{
trayicon = new NotifyIcon() { Icon = new Icon(#"D:\projects\Icon.ico"), Text = "Waiting", Visible = true };
trayicon.Visible = true;
Application.Run();
}
private static void Arg_Call(string args)
{
trayicon.Invoke((MethodInvoker)delegate {
trayicon.Text = "OK";
}); //from: https://stackoverflow.com/a/661662/8199423
}
Where am I wrong? How to and what is the best way to change the NotifyIcon.Text property in the running form via command-line-arguments?
I am sorry I was unable to adequately explain why your question is a duplicate of the existing "single-instance-application" questions. I will try to reiterate the train of thought here:
You wrote "How to and what is the best way to change the texts in the running form via command-line-arguments?"
Your requirement involves a currently-running process, which is presenting the NotifyIcon in the task tray, and the desire to use the command-line to modify that currently-running process's state.
It is a simple fact that when you type anything on the command line, it starts a whole new process. That process is necessarily different from the process that is already running, and which is presenting the NotifyIcon in the task tray.
Putting all of the above together, we have the conclusion that you want a new process that you start on the command line to interact with an existing process. And the simplest way to achieve that goal is to use the built-in single-instance-application support found in .NET. This is because the support for single-instance-applications includes automatic passing of the new command line arguments to the previous running program. Hence, the duplicate.
As I mentioned earlier, you should try to develop the skill to generalize and see how seemingly new problems are really just old problems in disguise and which you or someone else already knows how to solve.
In the same way that all problem solving can be summarized as "break the large problem down into smaller problems, repeat as necessary until all of the smaller problems are problems you already know how to solve", programming is very often not a matter of solving new problems, but rather of recognizing how your current problem is really a problem you already know how to solve.
All that said, I have the impression that you're still having difficulty figuring out how to apply that information to your specific scenario. So, perhaps this is an opportunity to illustrate the validity of the philosophy I espouse, by showing you how your seemingly different problem really is the problem I claim it is. :)
So, let's start with your original scenario. I am not using the code you posted, because it's mostly code that isn't needed. It seemed simpler to me to start from scratch. To do that, I wrote a little TrayManager class that encapsulates the actual NotifyIcon part of the functionality:
class TrayManager : IDisposable
{
private readonly NotifyIcon _notifyIcon;
public TrayManager()
{
_notifyIcon = new NotifyIcon
{
ContextMenu = new ContextMenu(new[]
{
new MenuItem("Exit", ContextMenu_Exit)
}),
Icon = Resources.TrayIcon,
Text = "Initial value",
Visible = true
};
}
public void Dispose()
{
Dispose(true);
}
public void SetToolTipText(string text)
{
_notifyIcon.Text = text;
}
protected virtual void Dispose(bool disposing)
{
_notifyIcon.Visible = false;
}
private void ContextMenu_Exit(object sender, EventArgs e)
{
Application.ExitThread();
}
~TrayManager()
{
Dispose(false);
}
}
The above hard-codes the context menu for the icon. Of course, it a real-world program, you'd probably want to decouple the menu from the above class, for greater flexibility.
The simplest way to use the above would look something like this:
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main(string[] args)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
using (TrayManager trayManager = new TrayManager())
{
Application.Run();
}
}
}
So, how do we modify the above so that when you run the program again, you can change the Text property of the NotifyIcon with the command-line arguments you type? That's where the single-instance application comes in. As seen in the duplicate I marked earlier, What is the correct way to create a single-instance application?, one of the simplest ways to accomplish this is to use the Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase class, which has built right in support for single-instance applications and a mechanism for delivering new command line arguments to the existing process.
The one little draw-back is that this class was designed for Winforms programs, with the assumption that there will be a main form. To use it will require creating a Form instance. For a program without the need for an actual form, this means creating a Form instance that is never shown, and making sure that it's never shown does require a little bit of finagling. Specifically:
class TrayOnlyApplication : WindowsFormsApplicationBase
{
public TrayOnlyApplication()
{
IsSingleInstance = true;
MainForm = new Form { ShowInTaskbar = false, WindowState = FormWindowState.Minimized };
// Default behavior for single-instance is to activate main form
// of original instance when second instance is run, which will show
// the window (i.e. reset Visible to true) and restore the window
// (i.e. reset WindowState to Normal). For a tray-only program,
// we need to force the dummy window to stay invisible.
MainForm.VisibleChanged += (s, e) => MainForm.Visible = false;
MainForm.Resize += (s, e) => MainForm.WindowState = FormWindowState.Minimized;
}
}
The only thing in the above that gives us the single-instance application behavior we want is the setting of IsSingleInstance = true;. Everything else is there just to satisfy the requirement that some Form object is present as the MainForm, without actually showing that object on the screen.
Having added the above class to the project, we can now "connect the dots". The new Program class looks like this:
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main(string[] args)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
using (TrayManager trayManager = new TrayManager())
{
TrayOnlyApplication app = new TrayOnlyApplication();
app.StartupNextInstance += (s, e) => trayManager
.SetToolTipText(e.CommandLine.Count > 0 ? e.CommandLine[0] : "<no value given>");
app.Run(args);
}
}
}
You'll note two changes:
In addition to the TrayManager, which handles the NotifyIcon, we now also create the TrayOnlyApplication object, subscribing to its StartupNextInstance event so that we can receive the command line arguments given to any new instance, and use that to set the Text property of the NotifyIcon object (by passing that to the method created specifically for that purpose).
Instead of using Application.Run() to run the require message-pump loop to handle window messages, we use the Run() method our TrayOnlyApplication class inherited from the WindowsFormsApplicationBase class. Either of these methods handle message pumping while the program is running, and return control to the caller when the Application.ExitThread() method is called, so both approaches to message pumping work with the code in the TrayManager.
Now, the above example is simply a slight modification of the original version that didn't enforce single-instance operation. You might notice that it has the arguable deficiency that it always creates the tray icon, whether or not it's the first instance to run. Subsequent instances will run, create the tray icon, then immediately dismiss the icon and exit.
The WindowsFormsApplicationBase provides a mechanism to avoid this, the Startup event. While the StartupNextInstance event is raised in any instance of the application that is run when an instance already is running, the Startup event is raised only when no other instance is already running. I.e. in the instance where you actually want to do things, like show the tray icon.
We can take advantage of that event to defer creation of the NotifyIcon until we know whether we actually need it or not:
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main(string[] args)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
TrayManager trayManager = null;
TrayOnlyApplication app = new TrayOnlyApplication();
// Startup is raised only when no other instance of the
// program is already running.
app.Startup += (s, e) => trayManager = new TrayManager();
// StartNextInstance is run when the program if a
// previously -run instance is still running.
app.StartupNextInstance += (s, e) => trayManager
.SetToolTipText(e.CommandLine.Count > 0 ? e.CommandLine[0] : "<no value given>");
try
{
app.Run(args);
}
finally
{
trayManager?.Dispose();
}
}
}
Note that here, we need to write the try/finally explicitly instead of using the using statement, because the using statement requires initialization of the variable when it's declared, and we want to defer initialization until later, or never, depending on which instance is being run.
(Unfortunately, there's no way to defer creation of the dummy window in the TrayOnlyApplication class, since it's required just to call the Run() method, which requires a valid Form object be already set, and the determination as to which instance is being run happens in that call, not before.)
And that's all there is to it. The above shows, clearly I hope, exactly how the single-instance application techniques available to you directly solve the problem you are asking for help with. By providing a mechanism for a newly-run instance of your program to communicate the command line arguments passed to it, to the already-running instance of the same program, that newly-run instance can cause the already-running instance to perform whatever work it needs to (such as changing the tool-tip text for the tray icon, for example).
Naturally, any similar mechanism will achieve the same result. The only important thing is to have the newly-run instance detect an existing instance, and communicate with it. It just happens that the WindowsFormsApplicationBase class provides that functionality pre-made. There are lots of other ways to do the same thing, each with their own pros and cons.

Make main thread execute code on button press after form.show

I have a piece of code that does some calculations and then calls the form.show command. Now I have a library (the revit api) that does not allow me to store variables in a project without being in the main thread.
The logical solution for this is to get the spawned thread to call the main thread using say a producer/consumer pattern with code looking a bit like this:
form.Show(owner);
while(AppIsRunning){
if(clicked)
commit();
else
Thread.sleep(100);
}
However when I do this the gui does not load fully (black background, no text in buttons ext.).
I have also tried doing this using the evoke method
private void BtnOK_Click(object sender, System.EventArgs e)
{
Commit();
Invoke(Commit);
}
private void Invoke(Action commit)
{
commit.Invoke();
}
However this just tells me that it's not the main thread that's executing the commit function.
Is there another way to do this or am I just making an error.
Just to be clear I have a form.show(owner) command that throws an error if it's not executed by the main thread. I also have a commit() function that must be excused by the main thread or it throws an error. The execution must wait until a button press. But the main thread polling the gui thread for changing causes the program to hang. According to my google search it' s also possible to do something involving an external event to get back into the right context but the example given was using python to invoke c# code, is there a good way to raise an external event to get back into a given thread in c#?
Edit: based on some suggestions I have created the following code:
public class ThreadManager
{
static List<ThreadAble> orders = new List<ThreadAble>();
public static bool running = false;
public static void execute(ThreadAble action)
{
orders.Add(action);
}
static System.Timers.Timer timer;
public static void RegisterAPIThreadAndHold(ExternalCommandData commandData)
{
UIApplication uiapp = commandData.Application;
uiapp.Idling += Application_Idle;
}
private static void Application_Idle(Object o,IdlingEventArgs e)
{
if (orders.Count != 0)
{
ThreadAble f = orders.First();
orders.Remove(f);
f.execute();
}
}
}
public interface ThreadAble {
void execute();
}
However this does not appear to actually run when I use it as
public override Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
Form frm = new OverviewForm(ExternalCommandData commandData);
frm.show()
ThreadManager.RegisterAPIThreadAndHold(commandData);
ThreadManager.Execute(new run_ThrowError())
where ThrowError.execute() is
Throw new Exception(" this is actually being executed" );
Your first example could work if you will replace Thread.Sleep by the System.Windows.Forms.Application.DoEvents(). It should give time to paint GUI and do not froze application completly.
form.Show(owner);
while(AppIsRunning){
if(clicked)
commit();
else
{
System.Windows.Forms.Application.DoEvents();
// Thread.sleep(100);
}
}
But this is not perfect solution to achieve this.
Better would be calling Dispatcher.Invoke command inside your dialog to perform MainThread operations.
You can use i.e. GalaSoft library - please refer to DispatcherHelper object documentation and samples.
The two ways to do this I'm aware of are with the External Event or the Idling event.
With the idling event, you'll register it, and while it is registered, your code (in the main thread) will get a callback from Revit every time that it's not busy with something else. Often a few times per second.
Once you are in the Idling callback, then you're able to create transactions and interact with the model. So your callback checks the state of the form and decides whether there is something to do.
The External Event works similarly in terms of registration, but you're able to request a trigger of the callback.
Jeremy Tammik must have 20 posts on thebuildingcoder.typepad.com on Modeless dialog / Revit stuff.
For a simple solution to this, please refer to the Revit SDK ModelessDialog ModelessForm_ExternalEvent sample application. It demonstrates exactly what you are asking for.

Not sure if I'm using "using" correctly c# in tftp app

I'm trying to use this pre-made C# tftp server app with my windows c# form. In the authors server example, which works great, he uses a console app. When I trying porting his console example into my form app it doesn't work (no errors, just doesn't connect) and I believe my issue is in the "using" statement:
using (var server = new TftpServer())
{
server.OnReadRequest += new TftpServerEventHandler(server_OnReadRequest);
server.OnWriteRequest += new TftpServerEventHandler(server_OnWriteRequest);
server.Start();
Console.Read();
}
Not sure if I understand correctly but I believe the Console.Read() blocks keeping the app from exiting. If this is the case how would I implement a equivalent with a form app. I just can't get my head around the "using". Sorry I'm new to c#.
Windows Forms will always remain open until they're explicitly closed by the user. They always have a thread reading the message queue for user input, so they won't exit the same way an unrestrained console application will. In Windows Forms, we have to worry a bit more about multithreading and concurrency than we would in console apps. It mostly comes naturally, but not always.
Because of that, you can't really use an equivalent to Console.Read() to hold off execution of the using disposal until the user requests it. If you did, your form would simply appear unresponsive.
However, you're in luck! A using block in C# is nothing more than syntactic sugar for remembering to call IDisposable.Dispose() after you're done with an object. So the equivalent to this in a Forms project could just be storing the server object in a class-wide field, then calling server.Dispose() on, say, a Button.Click event. That's, of course, just an example. You could also do it on Form.Closing if that felt more appropriate.
High-level, you want to do something like this:
Declare a field in your form class TftpServer server;.
Register a Load event and whatever you need for your server to function in your constructor.
Open your server field in the Form_Load event.
Use the server's events as you see so fit during the life of your Form. You may or may not have to worry about concurrency, but that's a matter for another question.
Call server.Dispose() in the form's Dispose event.
In essence,
class main : Form
{
private TftpServer server;
public main()
{
InitializeComponent();
this.Load += main_Load;
server = new TftpServer();
server.OnReadRequest += new TftpServerEventHandler(server_OnReadRequest);
server.OnWriteRequest += new TftpServerEventHandler(server_OnWriteRequest);
}
private void main_Load(object sender, EventArgs e)
{
server.Start();
}
private void server_OnReadRequest(/* I wasn't sure of the arguments here */)
{
// use the read request: give or fetch its data (depending on who defines "read")
}
private void server_OnWriteRequest(/* I wasn't sure of the arguments here */)
{
// use the write request: give or fetch its data (depending on who defines "write")
}
protected override void Dispose(bool disposing)
{
if (server != null) // since Dispose can be called multiple times
{
server.Dispose();
server = null;
}
}
}
The problem is that disposing the server is what is closing it. Keep in mind using is just syntactic sugar. The following two code chunks are [practically] equivalent:
var foo = new Foo();
try
{
foo.Do();
}
finally
{
foo.Dispose();
}
using (var foo = new Foo())
{
foo.Do();
}
You are fine blocking the main thread from exiting in a Console app, but in a Forms app it's different. The problem is not that you need to hold the thread inside the using by doing some sort of blocking operation. That would be bad, and the behavior would lock up your forms app. The problem is you don't want to use using. You want to new it up when you start the server, and then later on, on application exit, or on a stop click, explicitly dispose it with Dispose().
In a console application your TftpServer instance is listening until the thread exits which is only after a key is pressed which is detected by Console.Read()
In your forms app that Console.Read() isn't waiting around and so the using block finishes and that causes your server instance to fall out of scope.
So you are not exactly misusing the using but rather the intended use is not helping you at all. Take a look at using the task parallel library to let some background tasks run asynchronously.
A small note that also doubles as an answer, you could use a using block here, you just put it in your main function:
...(make your form and stuff)
using (var server = new TftpServer())
{
server.OnReadRequest += new TftpServerEventHandler(server_OnReadRequest);
server.OnWriteRequest += new TftpServerEventHandler(server_OnWriteRequest);
server.Start();
Application.Run(yourFormHere); //This blocks until the form is closed
}
Another option I forgot to mention is overriding Dispose in your Form. You probably want to do this. With this option you're guaranteed your server will be disposed (bar some event that would prevent it from being disposed either way [ie. being out of memory])

Winforms and Background Worker

I have a console app at the moment, which monitors a folder for files, and then based on rules and the file name, copies any new file to a location on the network.
I have a requirement to make the application more pretty, so decided to go with a simple WinForms single form application which displays status and 'last updated file' type information.
The console app was written in such a way that all Console display information went through a single method, which I called 'Notify', taking two parameters. A string to display the information I want the user to see, and an ErrorLevel Enum, which, if 'Normal' displayed in green text, if Warning, was yellow, and if error, was red. But the point is, all my code just did was use the 'Notify' method to output any text.
I want to change my console app into a normal class, run it as a background worker from the WinForms project, and have the Notify method in the thread send updates to the winforms app, safely. I think it can be done with events, but I am not sure what would be the best way to handle this. Could you propose a method to get this working?
There's the 'Invoke' way of doing things. Is it good? Something like:
this.BeginInvoke (new MethodInvoker(() => UpdateLabel(s));
It seems it would be basic, but I'd like to still make use of my Notify method, and have that send messages to the UI layer.
I also need the console app to send messages to the thread. For example, 'Stop', where I then run code that gracefully quits the thread... and also, 'Refresh', which does some logic within the thread.
Another option is to run the processing class as a service? And then have a UI that somehow connects to the system service and gets updates? I have never done anything like that, but the process is meant to run all the time...
At the moment, I have my code running, but no updated to the UI:
public partial class MainForm : Form
{
BackgroundWorker _bw = new BackgroundWorker();
public MainForm()
{
InitializeComponent();
}
private void MainForm_Load(object sender, EventArgs e)
{
_bw.DoWork += bw_DoWork;
_bw.RunWorkerAsync();
}
private void bw_DoWork(object sender, DoWorkEventArgs e)
{
var bw = sender as BackgroundWorker;
var monitor = new Monitor();
monitor.RunMe();
}
}
I think I just need to find a way to get my Notify method in my thread to send a message or something (An object that I create, with Message String and ErrorCode properties?) back to my UI, and process it safely on the UI.
And here is the code within the class (thread)...
public class Monitor
{
public void RunMe()
{
Notify("Checking for network connectivity...", Constants.ErrorLevel.Information);
if (FileManagement.FolderExists(Constants.FolderToMonitor) == false)
{
Notify("Unable to monitor folder - Aborting.", Constants.ErrorLevel.Error);
Console.ReadKey();
return;
}
Notify("OK", Constants.ErrorLevel.Information);
....
}
Note: Readkey will be removed..
There may be a better approach, but if your Notify method needs to interact with the GUI, try using the ReportProgress event on the BackgroundWorker. You can pas an object as the state parameter, and probably just ignore the progress value.

Categories

Resources