0

I'm new to C# .Net and Visual Studio 2022 - What I'm trying to achieve is to have a timer running every second to check that a website url is valid/is up. If the url IS reachable and the current WebView2 is not showing that website, then it should navigate to it. If it's already showing that website, it should do nothing else. If it was showing that website, but now it's no longer valid, the WebView should navigate to my custom error page. If whilst on the custom error page the website becomes available again, it should (re)load the website. In my particular scenario I'm making a webView load localhost (127.0.0.1) for now. I want to continuously check the website is ip, and if it goes down, show custom error, if it comes back, show the website.

Not sure I'm explaining that very well. From the research I have done, I believe I need Task and also await using async method.

Here's my current timer and checkurl code as well as navigtionstarted and navigationcompeted:

private void webView_NavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs e)
{
    timerCheckRSLCDURL.Enabled = false;
}

private void webView_NavigationCompleted(object sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationCompletedEventArgs e)
{
    if (e.IsSuccess)
    {
        Debug.WriteLine("JT:IsSuccess");
        ((Microsoft.Web.WebView2.WinForms.WebView2) sender).ExecuteScriptAsync("document.querySelector('body').style.overflow='hidden'");
    }
    else if (!e.IsSuccess)
    {
        Debug.WriteLine("JT:IsNOTSuccess");
        webView.DefaultBackgroundColor = Color.Blue;
        //webView.CoreWebView2.NavigateToString(Program.htmlString);
    }
    timerCheckRSLCDURL.Enabled = true;
}

private void timerCheckRSLCDURL_Tick(object sender, EventArgs e)
{
    Debug.WriteLine("Timer Fired! Timer.Enabled = " + timerCheckRSLCDURL.Enabled);
    CheckURL(Properties.Settings.Default.URL, Properties.Settings.Default.Port);
}

private async void CheckURL(string url, decimal port)
{
    timerCheckRSLCDURL = false;
    Program.isWebSiteUp = false;
    string webViewURL = BuildURL();
    Debug.WriteLine("Checking URL: " + webViewURL);

    try
    {
        var request = WebRequest.Create(webViewURL);
        request.Method = "HEAD";
        var response = (HttpWebResponse) await Task.Factory.FromAsync < WebResponse > (request.BeginGetResponse, request.EndGetResponse, null);
        if (response.StatusCode == HttpStatusCode.OK)
        {
            Program.isWebSiteUp = true;
        }
    }
    catch (System.Net.WebException exception)
    {
        Debug.WriteLine("WebException: " + exception.Message);
        if (exception.Message.Contains("(401) Unauthorized"))
        {
            Program.isWebSiteUp = false;
        }
        else
        {
            Program.isWebSiteUp = false;
        } // This little block is unfinished atm as it doesn't really affect me right now
    }
    catch (Exception exception)
    {
        Debug.WriteLine("Exception: " + exception.Message);
        Program.isWebSiteUp = false;
    }

    if (Program.isWebSiteUp == true && webView.Source.ToString().Equals("about:blank"))
    {
        Debug.WriteLine("JT:1");
        Debug.WriteLine("isWebSiteUp = true, webView.Source = about:blank");
        webView.CoreWebView2.Navigate(webViewURL);
    }
    else if (Program.isWebSiteUp == true && !webView.Source.ToString().Equals(webViewURL))
    {
        Debug.WriteLine("JT:2");
        Debug.WriteLine("isWebSiteUp = true\nwebView.Source = " + webView.Source.ToString() + "\nwebViewURL = " + webViewURL + "\nWebView Source == webViewURL: " + webView.Source.ToString().Equals(webViewURL) + "\n");
        webView.CoreWebView2.Navigate(webViewURL);
    }
    else if (Program.isWebSiteUp == false && !webView.Source.ToString().Equals("about:blank"))
    {
        Debug.WriteLine("JT:3");
        Debug.WriteLine("This SHOULD be reloading the BSOD page!");
        webView.CoreWebView2.NavigateToString(Program.htmlString);
    }
}

private string BuildURL()
{
    string webViewURL;
    string stringURL = Properties.Settings.Default.URL;
    string stringPort = Properties.Settings.Default.Port.ToString();
    string stringURLPORT = $ "{stringURL}:{stringPort}";
    if (stringPort.Equals("80"))
    {
        webViewURL = stringURL;
    }
    else
    {
        webViewURL = stringURLPORT;
    }
    if (!webViewURL.EndsWith("/"))
    {
        webViewURL += "/";
    }
    //For now, the URL will always be at root, so don't need to worry about accidentally
    //making an invalid url like http://example.com/subfolder/:port 
    //although potentially will need to address this at a later stage

    Debug.WriteLine("BuildURL returns: " + webViewURL);

    return webViewURL;
}

So the timer is fired every 1000ms (1 second) because I need to actively check the URL is still alive. I think the way I'm controlling the timer is wrong - and I imagine there's a better way of doing it, but what I want to do is this...

  • Check website URL every 1 second
  • To avoid repeating the same async task, I'm trying to disable the timer so it does not fire a second time whilst the async checkurl is running
  • Once the async/await task of checking the url has finished, the timer should be re-enabled to continue monitoring is the website url is still up
  • If the website is down, it should show my custom error page (referred to as BSOD) which is some super basic html loaded from resources and 'stored' in Program.htmlString
  • if the the website is down, and the webview is already showing the BSOD, the webview should do nothing. The timer should continue to monitor the URL.
  • if the website is up and the webview is showing the BSOD, then it should navigate to the checked url that is up. If the website is up, and the webview is already showing the website, then the webview should do nothing. The timer should continue to monitor the URL.

From other research, I'm aware I shouldn't be using private async void - eg shouldn't be using it as a void. But I've not yet figured out / understood the correct way to do this

In the Immediate Window, it appears that webView_NavigationCompleted is being fired twice (or sometimes even a few times) instantly as the immediate window output will show JT:IsSuccess or JT:IsNOTSuccess a few times repeated in quick succession. Is that normal? I'm assuming something isn't correct there.

The main problem appears to be due to the timer being only 1 second. If I change the timer to fire every 30 seconds for example, it seems to work ok, but when it's every second (I may even need it less than that at some point) it's not really working as expected. Sometimes the BSOD doesn't load at all for example, as well as the webView_NavigationCompleted being fire multiple times in quick succession etc.

Could someone pretty please help me make this code better and correct. I've searched countless websites etc and whilst there is some good info, some of it seems overwhelming / too technical so to speak. I had to lookup what "antecedent" meant earlier as it's a completely new word to me! :facepalm:

Many thanks inadvance

ThomasArdal
  • 4,999
  • 4
  • 33
  • 73
IAmOrion
  • 37
  • 1
  • 10
  • 2
    Are you sure you want it to fire every single second? That's a lot of pressure you put on your application. Even setting it to 5 or 10 seconds is already lifting up the burden five or ten times, and giving it enough time to finish it's task. – Steven Jul 07 '22 at 12:36
  • @Steven - Perhaps I'll just set it to 5 seconds then, I'm impatient I guess ha. Does my code look ok then and I'm just not being realistic about the frequency? (Eg, 5 seconds or 10 seconds would work and be fine) – IAmOrion Jul 07 '22 at 13:07
  • 2
    `request` and `response` need `using`. Consider using a `static HttpClient _client` and `await _client.GetAsync` as this might perform better – Charlieface Jul 07 '22 at 13:58
  • @Charlieface - do you mean remove ```using System.Net;``` and then replace the the ```request``` and ```response``` somehow? (Just checking I understand what you meant) – IAmOrion Jul 07 '22 at 14:34
  • 1
    No, I mean a `using (var response = ....` to dispose it correctly – Charlieface Jul 07 '22 at 14:36
  • @Charlieface - Ah, I see. Ok, I'll have a look into it. I've no idea how to implement that off the top of my head, but like most of this project I'm sure I'll figure it out from other example uses :) Thanks for the heads up/info – IAmOrion Jul 07 '22 at 14:46
  • 1
    https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-statement – Charlieface Jul 07 '22 at 14:47

1 Answers1

2

This answer will focus on the Task timer loop to answer the specific part of your question "check a url is valid every second". There are lots of answers about how to perform the actual Ping (like How do you check if a website is online in C#) and here's the Microsoft documentation for Ping if you choose to go that route.

Since it's not uncommon to set a timeout value of 120 seconds for a ping request, it calls into question whether it would have any value to do this on a steady tick of one second. My suggestion is that it would make more sense to:

  1. Make a background thread
  2. Perform a synchronous ping (wait for the result) on the background thread.
  3. Marshal the ping result onto the UI thread to perform the other tasks you have laid out.
  4. Synchronously wait a Task.Delay on the background thread before performing the next ping.

Here is how I personally go about doing that in my own production code:

void execPing()
{
    Task.Run(() => 
    {
        while (!DisposePing.IsCancellationRequested)
        {
            var pingSender = new Ping();
            var pingOptions = new PingOptions
            {
                DontFragment = true,
            };
            // https://learn.microsoft.com/en-us/dotnet/api/system.net.networkinformation.ping?view=net-6.0#examples
            // Create a buffer of 32 bytes of data to be transmitted.
            string data = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
            byte[] buffer = Encoding.ASCII.GetBytes(data);
            int timeout = 120;
            try
            {
                // https://stackoverflow.com/a/25654227/5438626
                if (Uri.TryCreate(textBoxUri.Text, UriKind.Absolute, out Uri? uri)
                    && (uri.Scheme == Uri.UriSchemeHttp || 
                    uri.Scheme == Uri.UriSchemeHttps))
                {
                    PingReply reply = pingSender.Send(
                        uri.Host, 
                        timeout, buffer, 
                        pingOptions);
                    switch (reply.Status)
                    {
                        case IPStatus.Success:
                            Invoke(() => onPingSuccess());
                            break;
                        default:
                            Invoke(() => onPingFailed(reply.Status));
                            break;
                    }
                }
                else
                {
                    Invoke(() => labelStatus.Text = 
                        $"{DateTime.Now}: Invalid URI: try 'http://");
                }
            }
            catch (Exception ex)
            {
                // https://stackoverflow.com/a/60827505/5438626
                if (ex.InnerException == null)
                {
                    Invoke(() => labelStatus.Text = ex.Message);
                }
                else
                {
                    Invoke(() => labelStatus.Text = ex.InnerException.Message);
                }
            }
            Task.Delay(1000).Wait();
        }
    });
}

What works for me is initializing it when the main window handle is created:

protected override void OnHandleCreated(EventArgs e)
{
    base.OnHandleCreated(e);
    if (!(DesignMode || _isHandleInitialized))
    {
        _isHandleInitialized = true;
        execPing();
    }
}
bool _isHandleInitialized = false;

Where:

private void onPingSuccess()
{
    labelStatus.Text = $"{DateTime.Now}: {IPStatus.Success}";
    // Up to you what you do here
}

private void onPingFailed(IPStatus status)
{
    labelStatus.Text = $"{DateTime.Now}: {status}";
    // Up to you what you do here
}

public CancellationTokenSource DisposePing { get; } = new CancellationTokenSource();

ping result x 3

Example 404:

No such host is known

IVSoftware
  • 5,732
  • 2
  • 12
  • 23
  • 1
    Wow, this is a super helpful answer especially with the screenshots. One thing to note though, I had seen that SO thread before, I decided not to use ping since it only really checks the network connection so to speak. Eg, I could ping another device on my network, and it would say it is valid since it's pingable, but in reality, that device is not a web server therefore serving no pages, hence I went with Digicoders answer of doing an httpwebrequest - that way I can distinguish between no web server (unable to connect), 401 etc etc – IAmOrion Jul 07 '22 at 16:20
  • 1
    Oh I agree for sure! The reason I chose to link that _question_ is that it contains [this answer](https://stackoverflow.com/a/7524060/5438626) that does just what you say (quoting the answer) "A Ping only tells you the port is active, it does not tell you if it's really a web service there. My suggestion is to perform a HTTP HEAD request against the URL" so substitute that way of doing things and this loop will still work :) – IVSoftware Jul 07 '22 at 16:23
  • 1
    [Clone](https://github.com/IVSoftware/ping-task-vs22.git) this example and experiment! – IVSoftware Jul 07 '22 at 16:28
  • gotchya, great, I will give it a try later tonight and report back (I need to wrap my head around adjusting that loop to use the HttpWebRequest method, but I think I'll figure out - plus it's part of the fun of learning when given steered the right way :) ) – IAmOrion Jul 07 '22 at 16:35
  • 1
    No problem! And I just remembered that your original question asked about `async` so I added a separate branch to the repo with minor changes that uses `Task.Run(async() =>...` instead of `Task.Run(()=>...` which lets you do things like `await pingSender.SendPingAsync(...)` and `await Task.Delay(1000)` inside the loop. – IVSoftware Jul 07 '22 at 17:08
  • 1
    Assuming your loop does what's needed and is non-blocking I guess I don't specifically need it async? (non blocking is important since the UI still need to be able to be interacted with such as moving the window, changing settings etc) – IAmOrion Jul 07 '22 at 17:46
  • 1
    That is _exactly_ correct in my view! Very little to gain by doing that. – IVSoftware Jul 07 '22 at 17:48
  • giving this a go now, I swear VS2022 is so frustrating at times. When I created my project - for whatever reason, it defaults to C# 7 and .NET 4.8 -- I'm unable to change it after the event, so basically your code doesn't work for me in my main project as I'm plagued by errors such as ```Feature 'nullable reference types' is not available in C# 7.3. Please use language version 8.0 or greater.``` and ```The name 'lblStatus' does not exist in the current context``` and ```Cannot convert lambda expression to type 'Delegate' because it is not a delegate type``` and 50 other errors :( – IAmOrion Jul 07 '22 at 23:18
  • I should've rephrased my comment - Your code works fine as a new project just opening it up. The problem is it's not compatible with my existing project. MS really like to make it hard work - there's ```Windows Forms App for creating a .NET windows forms App```, and then there's ```Windows Forms App (.NET Framework)``` - they're different and the .NET Framework only goes up to .net 4.8 framework, whereas just .NET (no mention of framework) supports .NET 5.0 and .NET 6.0 Talk about confusing for newbies – IAmOrion Jul 07 '22 at 23:24
  • it's ok, I'll figure it out, you're approach is really good, I don't expect you to do the all work for me though, although I do really appreciate your help thus far – IAmOrion Jul 07 '22 at 23:28
  • 1
    [netcoreapp3.1](https://github.com/IVSoftware/ping-task-vs19.git) no trouble really. check out the `async` branch though, it's kinda fun IMO. – IVSoftware Jul 07 '22 at 23:45
  • I'm one step closer! The code goes into my project with just 1 single issue now -- I still had ```Feature 'nullable reference types' is not available in C# 7.3. Please use language version 8.0``` preventing the build (it would fail) - however I've fixed that now by doing this https://stackoverflow.com/a/56267555/365393 Works great, thank you so much. Now I just got to convert it to the httpwebrequest thing instead of ping ha - wish me luck. I will mark your response as accepted answer. – IAmOrion Jul 08 '22 at 00:06
  • I just realised I have one other question that you'll probs know the answer to straight away! ... using your code loop (I've converted the required parts to ```HttpWebRequest request``` and it works perfectly!) Thank you so much again!! My additional question however is this... I have a settings button, which will then show the settings dialog. How can I "Pause" the task when opening the settings dialog, and then "Resume" when the settings dialog closes? (I already have my functions to deal with opening, and then handling closing and what to do upon close etc) – IAmOrion Jul 08 '22 at 01:36
  • 1
    I'm hoping I anticipated your question ;) Have you looked at the `async` branch? It uses a `SemaphoreSlim` synchronization object to switch between the "ping" functionality and a long-ish running UI update task. Check it out and let me know if it helps. – IVSoftware Jul 08 '22 at 01:59
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/246262/discussion-between-iamorion-and-ivsoftware). – IAmOrion Jul 08 '22 at 02:04