Home > Enterprise >  Why would my timer does not dynamically display the file size?
Why would my timer does not dynamically display the file size?

Time:11-07

So basically, I m making a program where 25 tasks write the same random task to one file until it reaches a certain size, and I would like to dynamically display the file size of the selected file in 0.5 seconds interval, so I made a timer hooked with Timer_Elapsed which should be executed every 0.5 seconds and display on UI which I specify on mainWindow.xaml on the textblock x:Name="fileSize", so I placed the createTimer function in to btnGo_Click function in mainwindow.xaml.cs so the event would extract the right fileInfo of the selectedFile. Any advice for my wrong would be appreciated. I'm also sharing the FileIO class in case it is needed, so they are full solutions. Even aside from the questions I asked, any general advice to better my code would be appreciated because I need to get a grasp of the good code example.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Windows;
using System.Threading;

namespace WriteFile
{
    internal class FileIO
    {
        private object _lock = new object();
        public volatile bool sizeReached = false;
        private StreamWriter sw = null;
        Mutex mut = null;

        public FileIO()
        {
            if (!Mutex.TryOpenExisting("MyMutex", out mut))
            {
                mut = new Mutex(true, "MyMutex");
                mut.ReleaseMutex();
            }
        }

        internal void WriteFile(string FilePath)
        {
            while (!sizeReached)
            {
                mut.WaitOne();
                    try
                    {
                        using (sw = new StreamWriter(FilePath, true))
                        {
                            sw.WriteLine(Guid.NewGuid());
                        }
                    }
                    catch (Exception ex)
                    {
                        MessageBox.Show("Exception: "   ex.Message);
                    }

                    finally
                    {
                        if (sw != null)
                        {
                            sw.Close();
                        }
                    }
                mut.ReleaseMutex();              
            }
        }

        internal void SizeMonitor(string FPath, int MaxSize, Task[] tasks)
        {
            FileInfo fi = null;
            while (!sizeReached)
            {
                if (File.Exists(FPath))
                {
                    fi = new FileInfo(FPath);

                    if (fi.Length >= MaxSize)
                    {
                        sizeReached = true;
                    }
                }

                if (sizeReached)
                {
                    foreach (Task task in tasks)
                    {
                        task.Wait();
                    }
                }
                Thread.Sleep(1);
            }

            MessageBox.Show(fi.Length.ToString());
            MessageBox.Show("Done");
        }
    }

}

mainWindow.xaml

<Window x:Class="WriteFile.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WriteFile"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <TextBlock x:Name="fileSize"/>
        <TextBox Name ="TargetSize"  VerticalAlignment="Center" FontSize="20">
        </TextBox>
        <Label Content="Target Size" HorizontalAlignment="Left" Margin="92,150,0,0" VerticalAlignment="Top"/>
        <Button Name ="btnGo" Content="Write to file" HorizontalAlignment="Left" Margin="92,267,0,0" VerticalAlignment="Top" Width="100" Click="btnGo_Click"/>
    </Grid>
</Window>

mainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.IO;
using Microsoft.Win32;
using System.ComponentModel;
using System.Timers;
using System.Threading;
using System.Runtime.CompilerServices;

namespace WriteFile
{
    public partial class MainWindow : Window
    {
        System.Timers.Timer timer = new System.Timers.Timer();
        Task[] tasks;
        Task MonitorTask;
        static FileIO fio = new FileIO();
        static string fPath;
        static FileInfo fileInfo;

        public MainWindow()
        {
            InitializeComponent();
            CreateTimer();
        }

        public void CreateTimer()
        {
            var timer = new System.Timers.Timer(500); // fire every 0.5 second
            timer.Enabled = true;
            timer.Elapsed  = Timer_Elapsed;
        }

        private void Timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            fileSize.Text = fileInfo.Length.ToString();
        }

        private void btnGo_Click(object sender, RoutedEventArgs e)
        {
            SaveFileDialog saveFileDialog = new SaveFileDialog();
            saveFileDialog.ShowDialog();
            Stream myStream;
            saveFileDialog.FilterIndex = 2;
            saveFileDialog.RestoreDirectory = true;

            if (File.Exists(saveFileDialog.FileName))
            {
                File.Delete(saveFileDialog.FileName);
            }

            if ((myStream = saveFileDialog.OpenFile()) != null)
            {
                StreamWriter sw = new StreamWriter(myStream);
                sw.Write(" your text");
                myStream.Close();
            }
            
            int NoOfTasks = 25;
            int Target = Convert.ToInt32(TargetSize.Text);

            fPath = saveFileDialog.FileName;
            tasks = new Task[NoOfTasks];

            fio.sizeReached = false;
            fileInfo = new FileInfo(fPath);

            for (int i = 0; i < NoOfTasks; i  ) 
            {
                tasks[i] = new Task(() => fio.WriteFile(fPath));
                tasks[i].Start();
            }

            MonitorTask = new Task(() => fio.SizeMonitor(fPath, Target, tasks));
            MonitorTask.Start();
        }
    }
}

CodePudding user response:

As mentioned by others, FileInfo.Length value is pre-cached. If the associated file has changed, the FileInfo requires to refresh those cached values. To accomplish this, you must explicitly call the FileInfo.Refresh method.
Furthermore, System.Threading.Timer executes the callback on a background thread. In WPF you can only reference a DispatcherObject (for example TextBlock) from a dispatcher thread (UI thread). For this reason you should use the DispatcherTimer. DispatcherTimer executes the callback on the dispatcher thread.

Additionally, you would want to stop the timer once the writing to the file is completed.

You should not use Task.Start. Instead of Task.Start and a Mutex you should simply await the Task. In your context, you can await a collection of Task objects using Task.WhenALl.
And instead of creating Task explicitly, you should use the async API of the StreamWriter.

You actually don't have concurrent code (neither would you benefit from it): the Mutex, declaration of a volatile field and the lock object are redundant. Also closing the StreamWriter explicitly in a finally block is not needed as you already use a using-block to handle the lifetime of the instance.

There are some more flaws in your code (some logical issues for instance). I have refactored your version to eliminate some of the issues for example by using the asynchronous StreamWriter API and async/await.
Because it is difficult to make any sense of your code (due to the lack of context) it is not possible to further improve it. For example, creating a single concatenated string (consisting of e.g. 25 values) would result in a single file write (and resource allocation), which would significantly improve the overall performance.

FileIO.cs

namespace WriteFile
{
    internal class FileIO
    {
        public bool IsSizeReached { get; private set; }

        public FileIO()
        {
        }

        internal async Task WriteToFileAsync(string filePath)
        {
            if (this.IsSizeReached)
            {
                 return;
            }

            using (var sw = new StreamWriter(filePath, true))
            {
                await sw.WriteLineAsync(Guid.NewGuid());
            }   
        }

        internal async Task SizeMonitorAsync(string fPath, int maxSize, IEnumerable<Task> tasks)
        {
            
            FileInfo fi = new FileInfo(fPath);
            this.IsSizeReached = fi.Length >= maxSize;
            if (this.IsSizeReached)
            {
                return;
            }

            await Task.WhenAll(tasks);            

            MessageBox.Show(fi.Length.ToString());
            MessageBox.Show("Done");
        }
    }
}

MainWindow.xaml.cs

namespace WriteFile
{
    public partial class MainWindow : Window
    {
        private DispatcherTimer Timer { get; }
        private FileIO Fio { get; }
        private FileInfo DestinationFileInfo { get; }

        public MainWindow()
        {
            InitializeComponent();
            this.Fio = new FileIO();
        }

        public void StartTimer()
        {
            this.Timer = new DispatcherTimer(
              TimeSpan.FromMilliseconds(500), 
              DispatcherPriority.Background, 
              OnTimerElapsed,
              this.Dispatcher);
            this.Timer.Start();
        }

        public void StopTimer() => this.Timer.Stop();

        private void OnTimerElapsed(object sender, EventArgs e)
        {
            this.fileSize.Text = this.DestinationFileInfo.Length.ToString();
        }

        private async void btnGo_Click(object sender, RoutedEventArgs e)
        {
            SaveFileDialog saveFileDialog = new SaveFileDialog();
            saveFileDialog.ShowDialog();
            saveFileDialog.FilterIndex = 2;
            saveFileDialog.RestoreDirectory = true;

            if (File.Exists(saveFileDialog.FileName))
            {
                File.Delete(saveFileDialog.FileName);
            }

            Stream myStream;
            if ((myStream = saveFileDialog.OpenFile()) != null)
            {
                // Dispose StreamWriter and underlying Stream
                using (var sw = new StreamWriter(myStream))
                {
                  await sw.WriteAsync(" your text");
                }
            }
            
            int noOfTasks = 25;

            // TODO::You must validate the input to prevent invalid or unreasonable values.
            // Currently, this line will throw and crash the application if the input is invalid (not numeric).
            int target = Convert.ToInt32(TargetSize.Text);

            string fPath = saveFileDialog.FileName;
            var tasks = new List<Task>();
            this.DestinationFileInfo = new FileInfo(fPath);

            StartTimer();
            for (int i = 0; i < noOfTasks; i  ) 
            {
                Task writeToFileTask = this.Fio.WriteToFileAsync(fPath);
                tasks.Add(writeToFileTask);
            }

            await this.Fio.SizeMonitorAsync(fPath, target, tasks));
            StopTimer();
        }
    }
}

CodePudding user response:

Timers.Timer works asynchronously and it raises the Elapsed event on the pool thread if SynchronizingObject is not set. And work with WPF UI elements can only be done on the thread of their Dispatcher (almost always this is the main UI thread of the application).

Two solutions out of many possible:

        private void Timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            // Asynchronously getting the right data
            string text = fileInfo.Length.ToString();

            // Using the Window's Dispatcher
            // to setting ready-made data to UI elements.
            Dispatcher.BeginInvoke(() => fileSize.Text = text);
        }

The second option is without using a timer:

        public MainWindow()
        {
            Initialized  = RefreshAsync;
            InitializeComponent();
        }
        public async void RefreshAsync(object? sender, EventArgs e)
        {
            while(true)
            {
                string text = await Task.Run(() => fileInfo.Length.ToString());
                fileSize.Text = text;
                await Task.Delay(500);
            }
        }

  • Related