2

Background

There is an external program which I do not control that writes tab-delimited lines to a "PRN" file (Test_1234.prn) every 8 seconds. My program's job is read that file and write the nth line (the last line written to the file) to a simple ListView of Samples. I also run a StopWatch in the view and I had plans to use the clock to drive the reading of the PRN by checking the seconds of the StopWatch. Every 4 seconds I would go out and read the file and return a DataTable and then every 8 seconds I would write the nth line of that DataTable to my View. The PRN does get hold a lot of lines but the size never gets larger than a 1mb - 5mb.

The Problem

My problem stems from the fact that this is a synchronous operation, and while it works, it works only for a few minutes and the StopWatch behaves erratically and invokes the writing of multiple lines to my SamplesView. This ultimately causes my _nextLine counter to get ahead of where the actual PRN is and I get an out-of-bounds exception.

Given a lack of threading experience, I dont know where to start in using threads to fix this code so that it does what it is supposed to at the interval it is supposed to.

Since the Samples collection needs to be updated at an interval, I suspect I will need to implement something such as a background thread that respects the right of the View to update itself on the main thread given what I have read here and here. But, again, I don't know where to start.

Can someone help with implementing a threaded solution to fix my specific issue?

//Xaml belongs to my CalibrationView.xaml
<StackPanel Orientation="Vertical">
    <Label Content="Run Time:" FontSize="16" FontWeight="Bold" Margin="10,0,0,0"/>
    <TextBlock Name="ClockTextBlock" Text="{Binding CurrentTime, Mode=TwoWay}"/>
    <ListView ItemsSource="{Binding Samples}" SelectedItem="{Binding SelectedSample}">
        <ListView.View>
            <GridView>
                <GridViewColumn Header="Time" Width="70" DisplayMemberBinding="{Binding Time}"/>
            </GridView>
        </ListView.View>
    </ListView>
</StackPanel>

public class CalibrationViewModel : ViewModelBase
{
    DispatcherTimer dt = new DispatcherTimer();
    Stopwatch stopWatch = new Stopwatch();

    private bool _firstTime = true;
    private int _nextLine = 1;
    private int _sampleCount;

    public CalibrationViewModel(Calibration calibration)
    {
        Samples = new ObservableCollection<StepRecord>();

        dt.Tick += dt_Tick;
        dt.Interval = new TimeSpan(0, 0, 0, 1);
    }

    public ObservableCollection<StepRecord> Samples { get; set; }
    public DataTable Prn { get; set; }

    public String CurrentTime
    {
        get { return _currentTime; }
        set
        {
            if (_currentTime != value)
            {
                _currentTime = value;
                OnPropertyChanged("CurrentTime");
            }
        }
    }

    void dt_Tick(object sender, EventArgs e)
    {
        if (stopWatch.IsRunning)
        {
            TimeSpan ts = stopWatch.Elapsed;

            CurrentTime = String.Format("{0:00}:{1:00}:{2:00}", ts.Hours, ts.Minutes, ts.Seconds);

            if (ts.Seconds % 4 == 0)
            {
                // Custom parser that reads a Tab Delimited file and returns a DataTable (PRN)
                PrnParser parser = new PrnParser(); 

                Prn = parser.PopulateDataTableFromTextFile(@"C:\Users\Path\To\File\Test_1234.prn");

                if (_firstTime)
                {
                    _nextLine = Prn.Rows.Count - 1;
                    _firstTime = false;
                }
            }

            if (ts.Seconds % 8 == 0)
            {
                WriteLineToSamplesCollection();
            }
        }
    }

    private void WriteLineToSamplesCollection()
    {
        var record = new StepRecord
        {
            Time = (Prn.Rows[NextLine].Field<String>("Column-2")),
        };

        CurrentSample = record;

        Samples.Insert(0, record);

        _nextLine++;

        _sampleCount++;
    }
}
Isaiah Nelson
  • 2,450
  • 4
  • 34
  • 53
  • How about using a [FileSystemWatcher](http://msdn.microsoft.com/en-us/library/x7t1d0ky.aspx) and just get notified whenever the file is changed. – Clemens May 30 '13 at 16:16
  • @Clemens I thought about that, but I am not sure the event that FileSystemWatcher raises is on a separate thread or not. Plus, I would still need something to asynchronously read into that file and return the nth record to my samples collection so that it can be updated in the View. I am just not sure how to do that either. Seems like it would need to raise an event on a a separate thread so it could go out and do the work. Again, not sure where to start. – Isaiah Nelson May 30 '13 at 16:23
  • 1
    why is it that you form the `DataTable` at intervals of 4 seconds but only update the `ListView` `ItemSource` at 8? why not just form and update at 8 seconds. Am i missing something since it looks like the table created to `Prn` once every 8 seconds seems to be wasted pretty much. – Viv May 30 '13 at 16:39
  • @Viv The PRN file is constantly written to by the external program, so by reading the PRN and forming the DT at 4 seconds, I thought that would give it enough time to get the DT in between writing to the Samples collection. But your comment is giving me some pause as to why I am doing it. – Isaiah Nelson May 30 '13 at 16:50
  • @IsaiahNelson yeh firstly "magic numbers" such as estimating 4 seconds for a read operation is kinda considered bad(there are exceptions ofc). Secondly even if it was within 4 seconds, your logic of `% 4 == 0` is always going to run when `%8 == 0` thus forcing a new `DataTable` which makes the previous one useless. I do advice you to ditch the timer logic and use @Clemens's approach with your required tweaks. Also if you can use .net45 look into `await` (watch this http://channel9.msdn.com/Events/Build/2012/3-011) – Viv May 30 '13 at 17:27

1 Answers1

2

The following code shows a very basic example of how to use a FileSystemWatcher to monitor changes of a specific text file and read the last line whenever the file has changed. As the Changed event is already called in a seperate thread (from the ThreadPool) you don't need to care for asynchrony, except that you need to call the Dispatcher when you update your UI.

private FileSystemWatcher fsw;

public MainWindow()
{
    InitializeComponent();

    fsw = new FileSystemWatcher
    {
        Path = @"C:\Users\Path\To\File",
        Filter = "Test_1234.prn",
        NotifyFilter = NotifyFilters.LastWrite
    };

    fsw.Changed += (o, e) =>
        {
            var lastLine = File.ReadAllLines(e.FullPath).Last();
            Dispatcher.BeginInvoke((Action<string>)HandleChanges, lastLine);
        };

    fsw.EnableRaisingEvents = true;
}

private void HandleChanges(string lastLine)
{
    // update UI here
}

You will certainly need to improve this example in order to make the line reading more efficient. Don't read all lines every time the file has changed, but keep the reading position and start reading from the last kept position.

Clemens
  • 123,504
  • 12
  • 155
  • 268
  • So in your example is the `HandleChanges()` intended to be an example of how the Dispatcher would update my `ObservableCollection Samples` as I am currently doing with a call to `WriteLineToSamplesCollection()`? – Isaiah Nelson May 30 '13 at 16:58
  • I added the code from your example surrounding the Dispatcher.BeginInvoke in the constructor of a separate ViewModel and I didnt get any errors but in adding it to my CalibrationViewModel (as above) there is an error on Dispatcher.BeginInvoke, citing 'Cannot invoke non-static 'BeginInvoke' method in a static context. Why might this be? I read [Jon Skeet's](http://stackoverflow.com/questions/3760777/dispatcher-begininvoke-syntax) explanation on this error but it doesn't make sense why it would work in once case but not in the other. – Isaiah Nelson May 30 '13 at 21:21