Καλησπέρα παιδιά ...
Εδώ και πολύ καιρό συνηθίζουμε στη δουλεά να χρησιμοποιούμε διάφορα configuration files, στα οποία κάνουμε κάποια αρχική επεξεργασία για να παράγουμε objects, ui elements κτλ κτλ. Έτσι, μας δημιουργήθηκε η ανάγκη να cache-άρουμε το αποτέλεσμα αυτής της επεξεργασίας στη μνήμη, για να αποφεύγουμε το overhead της συνεχούς και concurrent χρήσης του σκληρού, πάνω στο ίδιο αρχείο.
Συνέπεια του απο πάνω όμως, ήταν η νέα ανάγκη να είμαστε σίγουροι οτι το cache-αρισμένο αποτέλεσμα ήταν το σωστό, με την έννοιά του οτι αταπολρίνεται στα πριεχόμενα του αρχικού file, όπως αυτά είναι τώρα ( συνηθίζουμε να κάνουμε αλλαγές σε αυτά τα αρχεία at runtime, κύρίως όταν είμαστε σε πελάτη για διάφορα customizations κτλ. ).
Σαν αποτέλεσμα, έγραψα μια μικρή κλάσση, η οποία αναλαμβάνει τα κάνει τα εξής:
1. Φορτώνει τα περιεχόμενα ενός File σε ένα MemoryStream ή byte[].
2. Κάνει μια επεξεργασία αυτών των δεδομένων, μέσω ενός delegate event το οποίο γίνεται raise κάθε φορά που ξαναδιαβάζεται το αρχείο.
3. Κάνει αυτά τα πράγματα αυτόματα, δεχόμενη events απο ένας FileSystemWatcher τον οποίο δημιουργεί κάθε φορά.
Συνήθως, ανάλογα το data type το οποίο παράγω απο το εκάστοτε αρχείο ή γενικό τύπο αρχείων (txt, xml, binaries), γράφω μια μικρή subclass, η οποία αναλαμβάνει να υλοποιήσει την specific επεξεργασία των περιεχομένων του αρχείου.
Ε, και τώρα είπα να μοιραστώ ... :P
Η χρήση του FileHandler είναι σχετικά απλή, χρειάζεται ένα όνομα, και ένα path σ'ένα αρχείο (ναί, κι εγώ έχω σκεφτεί οτι και ένα url θα ήταν καλή ιδέα εδώ). Το property Value επιστρέφει την τελευταία τιμή των περιχομένων του αρχείου, και έχει και ένα event, το οποίο καλείται κάθε φορά που γίνεται reload το αρχείο που παρακολουθεί.
static void Main(string[] args)
{
// make sure we stop running gracefully in Ctrl+C ...
Console.CancelKeyPress += new ConsoleCancelEventHandler(Console_CancelKeyPress);
// Create the file handler & set the notification method upon reload
FileHandler handler = new FileHandler("test", Path.Combine(System.Environment.CurrentDirectory, @"..\..\resources\test.txt"), true);
handler.OnFileLoaded += new FileHandler.ProcessFile(Reloaded);
// ok, now start asking for the value ...
while (_running)
{
Console.WriteLine(ReadString((byte[])handler.Value));
System.Threading.Thread.Sleep(new TimeSpan(0, 0, 30));
}
}
Εσωτερικά, η ψυχή του FileHandler είναι ένα MemoryStream, το οποίο κάθε φορά περιέχει τα περιεχόμενα του αρχείου σε bytes, ένας FileSystemWatcher ο οποίος παρακολουθεί το συγκεκριμένο αρχείο, και λίγη λογική η οποία αναλαμβάνει να θέτει ένα flag για το όταν χρειάζεται να φορτώσουμε ξανά το αρχείο.
Αυτά γίνονται κυρίως στη μέθοδο Initialize(string filePath):
/// <summary>
/// Performs whatever processing is required upon File load
/// </summary>
/// <param name="filePath"></param>
protected virtual void Initialize(string filePath){
// exit if we don't need initializing really ...
if (!m_RequiresInit) return;
// check for a file that exists
if (!File.Exists(filePath))
throw new ArgumentException(string.Format("Could not locate file: {0}", filePath));
// lock the object, and initialize
lock (this)
{
try
{
// store the path internally
m_FilePath = filePath;
// ok, now load up the file & set the file watcher
m_FileBuffer = new MemoryStream();
using (StreamReader reader = new StreamReader(File.OpenRead(m_FilePath)))
{
int byteVal;
while((byteVal = reader.Read()) != -1)
m_FileBuffer.WriteByte((byte)byteVal);
}
// ok, now we have the contents of the file in-memory as bytes. Process it if required.
if (null != this.OnFileLoaded)
this.OnFileLoaded(m_FileBuffer, this);
// mark we initialized succesfully
m_RequiresInit = false;
// handle the FileWatcher now ...
string fileDir = Path.GetDirectoryName(m_FilePath);
string fileName = Path.GetFileName(m_FilePath);
if (null == m_FileWatcher) m_FileWatcher = new FileSystemWatcher(fileDir);
m_FileWatcher.Filter = fileName;
m_FileWatcher.NotifyFilter = NotifyFilters.LastWrite;
m_FileWatcher.Changed += new FileSystemEventHandler(FileWatcher_Changed);
m_FileWatcher.EnableRaisingEvents = true;
// mark last update time
m_LastUpdate = (System.DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond);
}
catch {
// if something went wrong, mark as needing initialization
m_RequiresInit = true;
m_FileWatcher.EnableRaisingEvents = false;
throw;
}
}
}
Λίγη προσοχή μόνο χρειάζεται στη μέθοδο την οποία καλεί ο FileSystemWatcher, επειδή γυρίζει παραπάνω απο 1 events για κάθε φορά που αλλάζει το αρχείο που παρακολουθεί. Έτσι, κρατάω τα milliseconds την τελευταία φορά που κλήθηκε η μέθοδος, και αν η διαφορά με τα millis αυτή τη στιγμή είναι μικρότερη ενός threshold value αγνοώ το event.
private long m_LastUpdate = (System.DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond);
private long m_EventIgnoreTimeSpanMillis = 500;
/// <summary>
/// Executed whenever the file we're handling is changed
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void FileWatcher_Changed(object sender, FileSystemEventArgs e)
{
// we know already ! buzz off !
if (m_RequiresInit) return;
lock (this)
{
// Could it be that this is one of a sequence of events the FSWatcher annoyingly sends for each write ?
if(((System.DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond) - m_LastUpdate) < m_EventIgnoreTimeSpanMillis)
return;
// ok, we need re-initialization !
m_RequiresInit = true;
// however, if we need to do that immediately .. let's proceed !
if (m_InitImmediate) {
Initialize(m_FilePath);
}
}
}
Το μόνο που χρειάζεται ακόμη, είναι το property Value το οποίο επιστρεφει τα περιεχόμενα του αρχείου να ελέγχει πρώτα αν χρεάζεται να ξαναδιαβάσει το αρχείο. ( ο FileHandler έχει 2 modes λειτουργίας. Ξαναδιαβάζει το αρχείο αμέσως όταν έρθει event οτι άλλαξε, ή με lazy load όταν ζητηθεί η τιμή και τα περιεχόμενα έχουν αλλάξει, γι'αυτό υπάρχουν 2 constructors, και ένα property, το EnableLazyLoading )
/// <summary>
/// Indicates whether the FileHandler will reload the file
/// it handles immedaitely after any change, or wait until
/// its value is required
/// </summary>
public bool EnableLazyLoading {
get {
return m_InitImmediate;
}
set {
m_InitImmediate = value;
}
}
/// <summary>
/// Overridable member that returns the processed contents
/// of the file we're handling. By default, it will return the
/// contents of the file as a byte array
/// </summary>
public virtual object Value {
get {
// Check for needs initialization ...
if (m_RequiresInit)
Initialize(m_FilePath);
// ok, now return the contents of the file
return m_FileBuffer.ToArray();
}
}
... αυτά ... καλό πράμα τα χιόνια, σου ξεκλέβουν λίγο χρόνο απο τη δουλειά, που μας πάει 65-μία πλέον η "υπηρεσία" ... :P
Το attached project rar, είναι σε Visual C# Express 2005 ... δυστυχώς δεν έχω όλο το VS στο σπίτι.
Υ.Γ. Υποψιάζομαι οτι τα locks στον κώδικα θα μπορούσαν να γίνουν καλύτερα, όπως και ο μηχανισμός με τον delegate και τις virtual μεθόδους, έτσι ώστε αν βοηθά περισσότερο τον implementer μιας subclass .. ίσως επίσης να χρειαζόταν να "αδειάζει" αυτός ο buffer MemoryStream για να τρώει λιγότερα resources, αλλά αν αρχίσω να απαριθμώ τώρα βελτιώσεις, θα πρέπει να χιονίζει για 3 μήνες για να βρώ χρόνο να τις κάνω :D
Καλό βράδυ !
Angel
O:]