Press "Enter" to skip to content

Mini tutorial sull’uso di Timer e Queues per temporizzare l’esecuzione di task

Questo mini progetto è stato creato in risposta ad una domanda posta sui forum Microsoft Italia, Questa in cui è stato chiesto come fare a ritardare la lettura di un file utilizzando un timer e poi, approfondendo la richiesta, come fare a non eseguire l’operazione, in questo caso lo spostamento di uno o più files tutta in una volta ma processare un solo file per volta.

Ho suggerito l’uso dell’oggetto Queue ovvero coda, che permette di accodare oggetti e poi rimuoverli in modalità FIFO (First in, First out) in opposto all’oggetto Stack ovvero pila, dove gli oggetti sono ugualmente inseriti ed estratti ma in modalità LIFO (Last In, First Out).

L’Oggetto generic Queue, quindi Queue<T> permette di inserire elementi al suo interno usando il metodo Enqueue e di estrarli con il metodo Dequeue.

Ma vediamo cosa ho fatto.

 

L’interfaccia MainWindow.xaml

In questo caso la window è iper semplice, infatti il focus non è sulla User interface ma sul codice, quindi la nostra MainWindow, contiene un solo oggetto, una TextBox che ho messo in Binding con la stringa ResultText definita all’interno di MainWindow.xaml.cs (che sarà viewmodel di se stessa), in questa stringa semplicemente loggherò quello che i metodi chiamati dai 3 timer che gestiscono le attività eseguono.

 

<Window x:Class="DelayQueue.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:DelayQueue"
        mc:Ignorable="d"
        Title="Test Delayed operations" Height="350" Width="525">
    <Grid>
        <TextBox
            Text="{Binding ResultText, UpdateSourceTrigger=PropertyChanged}"
            HorizontalAlignment="Stretch"
            VerticalAlignment="Stretch"
            VerticalScrollBarVisibility="Auto"
            AcceptsReturn="True"
            AcceptsTab="True"
            TextWrapping="NoWrap"/>
    </Grid>
</Window>

Come potete vedere la window è semplicissima.

La classe Element.cs

Questa piccola classe, è l’oggetto che andiamo a scrivere sui file di testo che saranno generati dall’applicazione per la dimostrazione, è una semplice entity che implementa 2 property e il metodo ToString.

public string Value
{
	get
	{
		return mValue;
	}
	set
	{
		mValue = value;
		OnPropertyChanged(FLD_Value);
	}
}
public string Description
{
	get
	{
		return mDescription;
	}
	set
	{
		mDescription = value;
		OnPropertyChanged(FLD_Description);
	}
}
public override string ToString()
{
	StringBuilder sb = new StringBuilder();
	sb.AppendFormat("{0} {1}", Value, Description);
	return (sb.ToString());
}

Vi ho copiato solo la parte significativa della classe, nel codice a corredo ovviamente è completa di tutte le definizioni.

Vediamo ora invece il codice dietro a MainWindow, che svolge tutto il lavoro che ci serve.

MainWindow.xaml.cs

private void LoadList()
{
	Elements.Add(new Element() { Value = "Pears", Description = "The Pears are good" });
	Elements.Add(new Element() { Value = "Apples", Description = "The Apples are good" });
	Elements.Add(new Element() { Value = "Pineapples", Description = "The Pineapples are good" });
	Elements.Add(new Element() { Value = "Mango", Description = "The Mango are good" });
	Elements.Add(new Element() { Value = "Papaya", Description = "The Papaya are good" });
	Elements.Add(new Element() { Value = "Lemon", Description = "The Lemon are good" });
	Elements.Add(new Element() { Value = "Lime", Description = "The Lime are good" });
	Elements.Add(new Element() { Value = "Orange", Description = "The Orange are good" });
	Elements.Add(new Element() { Value = "Grapefruit", Description = "The Grapefruit are good" });
	Elements.Add(new Element() { Value = "Grapes", Description = "The Grapes are good" });
	Elements.Add(new Element() { Value = "Kiwi", Description = "The Kiwi are good" });
	Elements.Add(new Element() { Value = "Nuts", Description = "The Nuts are good" });
	Elements.Add(new Element() { Value = "Peanuts", Description = "The Peanuts are good" });
	Elements.Add(new Element() { Value = "Pecan", Description = "The Pecan are good" });
	Elements.Add(new Element() { Value = "Hazelnuts", Description = "The Hazelnuts are good" });
	Elements.Add(new Element() { Value = "Banana", Description = "The Banana are good" });
	Elements.Add(new Element() { Value = "Watermelon", Description = "The Watermelon are good" });
 
}

Il primo metodo che vi mostro è quello che crea una lista di oggetti Element da cui saranno prelevati in modo Random degli elementi che saranno scritti su dei file di testo.

private string mDirectory;
private string mDirectoryMoveKo;
private string mDirectoryMoveOk;
private ObservableCollection<Element> mElements;
private DispatcherTimer mEnqueueTimer;
private DispatcherTimer mGenerateTimer;
private DispatcherTimer mDequeueTimer;
private Queue<string> mFilesToMoveKo;
private Queue<string> mFilesToMoveOk;
private string mResultText;
private Random mRnd;

Le variabili di classe, che utilizzeremo per le operazioni, sono dichiarate qui sopra e le loro descrizioni sono le seguenti:

 

  • Tre cartelle,
    • MDirectory  in cui genereremo dei file
    • MDirectoryMoveOk in cui sposteremo i file che contengono una delle tre parole: Apples, Orange, Banana
    • MdiRectoryMoveKo in cui sposteremo i file che non contengono una delle tre parole qui sopra indicate.
  • La collezione Elements, che contiene gli Elelement che useremo per generare i file di testo.
  • Tre timer.
    • mGenerateTimer, che temporizza la generazione dei file
    • mEnqueueTimer, che temporizza il controllo e l’accodamento dei file generati alle due code per lo spostamento.
    • mDequeueTimer, che temporizza il disaccodamento dei file generati dalla coda di spostamento e il loro spostamento sulle cartelle relative.
  • Due Queue<string> code di stringhe
    • mFilesToMoveKo, file non contenenti le parole cercate
    • mFilesToMoveOk, file contenenti le parole cercate
  • ResultText e mResultText, la variabile che visualizza il log di quello che accade nel codice
  • Un generatore di numeri casuali Random mRnd che servirà a generare casualmente gli indici della collection Elements per produrre dei file con dati casuali.
public MainWindow()
{
	InitializeComponent();
	DataContext = this;
	Elements = new ObservableCollection<Element>();
	LoadList();
	mGenerateTimer = new DispatcherTimer();
	mGenerateTimer.Interval = TimeSpan.FromSeconds(2);
	mGenerateTimer.Tick += MGenerateTimer_Tick;
	mGenerateTimer.Start();
 
	mEnqueueTimer = new DispatcherTimer();
	mEnqueueTimer.Interval = TimeSpan.FromSeconds(5);
	mEnqueueTimer.Tick += MEnqueueTimer_Tick;
	mEnqueueTimer.Start();
 
	mDequeueTimer = new DispatcherTimer();
	mDequeueTimer.Interval = TimeSpan.FromSeconds(3);
	mDequeueTimer.Tick += MDequeueTimer_Tick;
	mDequeueTimer.Start();
 
	mRnd = new Random(DateTime.Now.Millisecond);
	mFilesToMoveOk = new Queue<string>();
	mFilesToMoveKo = new Queue<string>();
	mDirectory = "C:\\Dotnetwork\\TimerTest";
	mDirectoryMoveOk = "C:\\Dotnetwork\\TimerTest\\MoveOk";
	mDirectoryMoveKo = "C:\\Dotnetwork\\TimerTest\\MoveKo";
	if (!Directory.Exists(mDirectory))
	{
		Directory.CreateDirectory(mDirectory);
	}
	if (!Directory.Exists(mDirectoryMoveKo))
	{
		Directory.CreateDirectory(mDirectoryMoveKo);
	}
	if (!Directory.Exists(mDirectoryMoveOk))
	{
		Directory.CreateDirectory(mDirectoryMoveOk);
	}
}

Nel costruttore della classe, per prima cosa indichiamo a WPF che la window è ViewModel di se stessa indicando che il DataContext è la finestra stessa.

Poi generiamo la collezione degli Elements e la riempiamo.

Poi inizializziamo i tre timer.

Inizializziamo il generatore di numeri casuali.

Inizializziamo le due code.

Assegnamo le directory alle variabili e se non esistono le generiamo.

Fatto questo, vediamo cosa facciamo nei tre metodi che rispondono ai timer:

private void GenerateFile()
{
	string name = Path.Combine(mDirectory, (Path.GetFileNameWithoutExtension(Path.GetRandomFileName()) + ".txt"));
	using (System.IO.StreamWriter file =
			new System.IO.StreamWriter(name))
	{
		for (int i = 0; i < 5; i++)
		{
			int ndx = mRnd.Next(0, Elements.Count - 1);
			file.WriteLine(Elements[ndx].ToString());
		}
	}
	ResultText = string.Format("Generated: {0}", name) + Environment.NewLine + ResultText;
}

Il metodo che genera i file di testo, semplicemente fa un ciclo di 5 volte e scrive in un file il contenuto di un elemento a caso ogni volta.

private void EnqueueFile()
{
	List<string> filesToCheck = new List<string>(Directory.GetFiles(mDirectory, "*.txt"));
	foreach (string fileName in filesToCheck)
	{
		if (IsFileinUse(new FileInfo(fileName)))
			continue;
 
		if (mFilesToMoveKo.Contains(fileName))
			continue;
		if (mFilesToMoveOk.Contains(fileName))
			continue;
		string text = File.ReadAllText(fileName);
		if (text.Contains("Apples") || text.Contains("Orange") || text.Contains("Banana"))
		{
			mFilesToMoveOk.Enqueue(fileName);
			ResultText = string.Format("ADD To OK: {0}", fileName) + Environment.NewLine + ResultText;
		}
		else
		{
			mFilesToMoveKo.Enqueue(fileName);
			ResultText = string.Format("ADD To KO: {0}", fileName) + Environment.NewLine + ResultText;
		}
	}
}

Il metodo che controlla quali file contengono i dati che ci interessano e quali no, e accoda i nomi dei file che interessano ad una coda, gli altri alla seconda.

private void DequeueFiles()
{
	if (mFilesToMoveOk.Count > 0)
	{
		lock (mFilesToMoveOk)
		{
			string moveOk = mFilesToMoveOk.Dequeue();
			string destOk = Path.Combine(mDirectoryMoveOk, Path.GetFileName(moveOk));
			File.Move(moveOk, destOk);
			ResultText = string.Format("Moved to OK: {0}", moveOk) + Environment.NewLine + ResultText;
		}
	}
	if (mFilesToMoveKo.Count > 0)
	{
		lock (mFilesToMoveKo)
		{
			string moveKo = mFilesToMoveKo.Dequeue();
			string destKo = Path.Combine(mDirectoryMoveKo, Path.GetFileName(moveKo));
			File.Move(moveKo, destKo);
			ResultText = string.Format("Moved to KO: {0}", moveKo) + Environment.NewLine + ResultText;
		}
	}
}

Il metodo che toglie dalle 2 code un singolo file spostandolo ad ogni giro del timer, per evitare che le chiamate effettuate possano entrare in conflitto (se la copia di un file durasse più dell’intervallo del timer) ho usato il lock sulle collection, in modo che in caso, la seconda chiamata attende.

 

Se eseguiamo il codice, vedremo il seguente risultato:

multi_timer_01

multi_timer_02

Riepilogo

In questo articolo abbiamo trattato i seguenti argomenti:

  • L’uso dei DispatcherTimer per eseguire operazioni temporizzate
  • L’uso dell’oggetto Queue per generare delle code
  • L’uso dei metodi Enqueue e Dequeue delle code
  • L’uso del generatore di numeri casuali
  • E l’uso del generatore di nomi di file casuali, da parte del generatore di file.

Potete scaricare il progetto esempio al link qui indicato:

Per qualsiasi domanda, commento, osservazione, approfondimento potete usare il link alla form di contatto in cima alla pagina.