Path validation helper in a ‘path input box’

Posted on

Problem

I have a helper extension method that I use for path string validation. It looks as below. It’s purpose is to work with network paths as well, so in case a network resource is not available, I do not want my applications to freeze, hence the specified timeout etc.

public static bool IsExistingFilePath(this string source, int numberOfAttempts = 3, int timeout = 300)
{
    for (int i = 0; i < numberOfAttempts; i++)
    {

        var task = new Task<bool>(() => File.Exists(source));
        task.Start();

        bool exists = task.Wait(timeout) && task.Result;
        if (exists)
        {
            return true;
        }
        else
            continue;
    }
    return false;
}

I also have a generic text box control, where I want to validate the provided path – and indicate if path is not correct. I want this auto text box validation to happen as infrequently as possible, because I don’t want to run this loop of File.Exists() tasks when the path is being provided at the moment (e.g. user typing) etc.

My current approach is that I have a property that is bound to textbox text, and whenever it is updated, the validation is performed (update source trigger = property changed, mode = two way).

However, there is a problem, because property setter is not accessed when I paste into the text box nor when I write in it. I had set up a bunch of event handlers (OnPaste and LostFocus), but it’s becoming unmanageable and appears not correct – and also appears to fire more times then necessary.

Then I thought about data validation from XAML with a ValidationRule class, but that actually works more or less like with KeyUp.

What would be the correct way to perform validation only when needed?

  • When text is pasted into box
  • When property is changed for whatever reason
  • When textbox is cleared
  • When text is changed (for instance, a proper path is pasted and a user decides to change it manually, e.g. use different drive)

Solution

Start a Timer.

Whenever the text box changes, the user presses a key, the TextChanged event occurs, etc, start the timer. If it changes again, reset it. Create some timeout that, after which, the file validation would occur. (200ms would be plenty)

This has the obvious benefit of reducing the number of IO requests you do, and it helps keep responsiveness up. You can also make it asynchronous, and only hold the UI when doing actual updating.

As far as your code goes…

I would add braces in your else condition. I used to omit braces whenever possible, but I encountered more than one bug as a result, and it’s just as easy to add them.

There’s really not much else to comment on; there’s not a lot going on here and it’s quite clean to begin with.

Regarding your IsExistingFilePath

  1. Starting a task and the immediately block (task.Result) is a huge antipattern. All the downsides and no benefit, the calling thread is freezing your UI now. Also there is overhead in creating running a task so it will block even longer. By doing this you open up the door for deadlocks and race conditions, all kinds of pain.
  2. When you start a task prefer Task.Run(() => ...) it is shorter and you can’t forget to call Start().
  3. Normally you don’t want to start a task to block for IO.
  4. If you use a task you should do async all the way.

let me post the ‘more or less’ complete solution to the problem that I applied based on EBrown’s answer (thanks). It appears to work pretty well, hope it helps someone in the future.

//timer that will control how often is the path validation logic executed
private readonly System.Timers.Timer _pathValidationTimer = new System.Timers.Timer(250) { AutoReset = false };

    //a constructor  of my user control with path validation logic
    public PathBox2()
    {
        InitializeComponent();

        _pathValidationTimer.Elapsed += (timerSender, args) =>
        {
            Dispatcher.BeginInvoke((Action)delegate
            {
                CheckPath();
                _pathValidationTimer.Stop();

            });
        };
    }


    private void PathBox_OnTextChanged(object sender, TextChangedEventArgs e)
    {
        //restarting the timer
        _pathValidationTimer.Stop();
        _pathValidationTimer.Start();

    }

    //the path check handler
    public bool CheckPath()
    {
        if (!CheckPaths)
            return true;
        //the control can be either a file path box or a folder box
        bool exists = IsFolderBox ? ProvidedPath.IsExistingDirPath(numberOfAttempts:1) : ProvidedPath.IsExistingFilePath(numberOfAttempts:1);

        if (!exists)
        {
            ConfirmationSignColorBrush = Brushes.DarkRed;
            ConfirmationTickLabel = "✘";
            return false;
        }
        else
        {
            UpdatePreviousPathsList(ProvidedPath); //it's a combobox with previous paths stored
            ConfirmationSignColorBrush = Brushes.Green;
            ConfirmationTickLabel = "✔";
            return true;
        }
    }

    //complete extension method for path validation
    public static bool IsExistingFilePath(this string source, bool showMsgIfMissing = false, int numberOfAttempts = 3, int timeout = 300, bool networkSafe = true)
    {
        if (networkSafe)
        {
            for (int i = 0; i < numberOfAttempts; i++)
            {
                var task = new Task<bool>(() => File.Exists(source));
                task.Start();
                bool exists = task.Wait(timeout) && task.Result;
                if (exists)
                {
                    return true;
                }
            }
            if (showMsgIfMissing)
            {
                Msg.Show(string.Format("Cannot find file:n{0}", string.IsNullOrWhiteSpace(source) ? "Path is empty" : source), "File not available", MsgBtn.OK, MsgImg.Warning);
            }
            return false;
        }
        else
        {
            if (File.Exists(source))
            {
                return true;
            }
            else
            {
                if (showMsgIfMissing)
                {
                    Msg.Show(string.Format("Cannot find file:n{0}", source), "File not available", MsgBtn.OK, MsgImg.Warning);
                }
                return false;
            }
        }
    }

Leave a Reply

Your email address will not be published. Required fields are marked *