WPF για τον τύπο Web μια ιστορία πόνου, απελπισίας και μάθησης
Λοιπόν, εντάξει είμαι πάνω από δραματική λίγο:) Όπως ίσως έχετε παρατηρήσει από μου τελευταία θέση Έχω αρχίσει να δουλεύω για κάποια πράγματα που είναι έξω από τη ζώνη άνεσής μου. Πρόσφατα μπήκα σε μια εταιρεία που ονομάζεται κοζβεκανική, εργάζονται για έναν τύπο που ονομάζεται @RobertTheGrey, το πρώτο project που δουλεύουμε είναι ένα site πωλήσεων φωτογραφιών...ξέρεις κάτι σαν Getty Images / Smugmug / Flickr εκτός από απευθείας πωλήσεις φωτογράφου-πελάτη. Προγραμματιστής πλήρους απασχόλησης (μέχρι στιγμής!) Έχω αναλάβει την οικοδόμηση της μεγάλης πλειοψηφίας των εφαρμογών (be they web, desktop, κλπ...) που χρειαζόμαστε για την επιχείρηση για να λειτουργήσει.
Προφανώς ένα από τα πιο κρίσιμα κομμάτια για ένα e-commerce site είναι πράγματα για να πουλήσει! Στην περίπτωσή μας αυτό είναι ένα απόθεμα πραγματικά υψηλής ποιότητας φωτογραφίες που λαμβάνονται από επαγγελματίες φωτογράφους σε όλο τον κόσμο. Πρώτη ερώτηση; Πώς μπορούμε να πάρουμε αυτές τις εικόνες στο site, και πώς αφήνουμε τους φωτογράφους να προσθέσουν πληροφορίες στις φωτογραφίες τους για να τους αφήσουμε να βρεθούν από τους ανθρώπους που θέλουν να τις αγοράσουν; Λοιπόν, αυτό είναι το πρώτο πρόβλημα που μου ζητήθηκε να λύσω!
Κατά την εργασία πάνω στις απαιτήσεις, έγινε γρήγορα σαφές ότι αυτό έπρεπε να είναι μια εφαρμογή επιφάνειας εργασίας, έπρεπε να επιτρέψει τις λειτουργίες σε πολυ-μεγάβυτε φωτογραφίες, που απαιτούνται για να επιτρέψει τη ρύθμιση της σύνδεσης. Ειδικά κατά την αρχική φόρτωση, υπάρχει 10-100 χιλιάδες εικόνες που εργάζονται σε...forcing αυτό για να είναι online προσθέτει μια δυσάρεστη καθυστέρηση και κάνει το έργο έναν πόνο στον κώλο για να ολοκληρωθεί. Πάνω από όλα, έπρεπε να είναι αξιόπιστη!
Τώρα, στο κεφάλι μου αυτό οδήγησε σε μια σειρά αποφάσεων σχετικά με τα χαρακτηριστικά που η εφαρμογή θα πρέπει να υποστηρίξει:
- Διαισθητική, ανταποκρίνεται UI; οι χρήστες δεν είναι απαραίτητα υπολογιστή-ευθυμία, πρέπει να χρησιμοποιούν προφανείς μεταφορές UI και να το κάνουν αρκετά γρήγορα για να είναι ευχάριστη στη χρήση.
- Χρειάζεται να κάνουμε κάποια επεξεργασία offline εικόνας; σχεδιάζουμε να το φιλοξενήσουμε αυτό στο σύννεφο... Ο χρόνος CPU κοστίζει μετρητά:)
- Θα πρέπει να δουλέψει εκτός σύνδεσης
- Χρειάζεται να χτιστεί γύρω από την αξιόπιστη φόρτωση πολλαπλών αρχείων meg (διασχίζοντας ενδεχομένως μικροσκοπικά εύρος ζώνης σύνδεσης).
Λοιπόν, αυτές είναι οι βασικές ανάγκες τεχνικά αυτό με οδηγεί σε κάποιες τεχνολογικές επιλογές:
- Multi-threating / concurrency (όπως με κάθε πραγματική εφαρμογή επιφάνειας εργασίας) είναι το κλειδίόπως είναι μια αξιοπρεπής τεχνολογία Desktop
- Δείτε παραπάνω, θα πρέπει να παρακολουθήσετε για νέα αρχεία που φθάνουν, να κάνουν τις μικρογραφίες διάφανα κλπ
- Έτσι, χρειάζεται κάποιο είδος επιμονής...μαζί με άλλες απαιτήσεις μη-core app (π.χ. tagging) θα χρειαστούμε πιθανώς λίγο, ελαφρύ DB.
- Αναγκάζει το μπλόκο που βασίζεται uploads με retry / επαλήθευση των αναρτημένων αντικειμένων
Ευτυχώς που ήμουν αφοσιωμένος για αρκετό καιρό και τουλάχιστον είχα κάποια ιδέα από πού να ξεκινήσω η μεγαλύτερη πρόκληση θα ήταν να μάθω!
Πρώτον, η προφανής πλατφόρμα επιφάνειας εργασίας de jour είναι WPF...αυτό είναι στην ουσία η τρέχουσα αντικατάσταση για WinForms, είναι πολύ προσαρμόσιμη αλλά έχει μια κάπως περίφημα απότομη καμπύλη μάθησης. βιβλία στις WPFCity name (optional, probably does not need a translation), ακολούθησε όλες τις παρουσιάσεις PDC, podcasts κ.λπ... σχετικά με το θέμα. Μετά από 3 εβδομάδες επιτέλους να πάρει άνετα με αυτό...
Δεύτερον, χρειαζόμουν ένα DB... που έπρεπε να είναι ελαφρύ, που χρειαζόταν για να υποστηρίξει LINQ (τι, μου αρέσει LINQ:)), που χρειαζόταν για να υποστηρίξει κάποιο είδος τεχνολογίας ORM.
Η πιο προφανής επιλογή ήταν SQL Server Compact Edition, ένα είδος κομμένα SQL server που είναι μεγάλη για εφαρμογές επιφάνειας εργασίας ... Παρ' όλα αυτά...Δεν μπορούσα να το κάνω να λειτουργήσει ! Αποδεικνύεται ότι είναι πραγματικά ευαίσθητο στις εκδόσεις των διαφόρων εγκατεστημένων συστατικών . Αυτό ήταν ένα κόκκινο φως για μένα ...Χρειαζόμουν το DB να είναι όσο το δυνατόν πιο αυτόνομο ! Αυτό άφησε μια προφανή επιλογή:
SQLite, από την ιστοσελίδα:
Η βιβλιοθήκη είναι μια βιβλιοθήκη λογισμικού που εφαρμόζει ένα Αυτοσυντηρούμενη, χωρίς serverless, μηδενική ρύθμιση@ info: whatsthis, συναλλαγή SQL κινητήρα βάσης δεδομένων. SQLite είναι το πιο διαδεδομένη SQL μηχανή βάσης δεδομένων στον κόσμο. Ο πηγαίος κώδικας για SQLite είναι στην δημόσιος τομέας.’
Τέλεια! Για .NET υποστήριξη, χρησιμοποίησα το SQLite ADO.NET πάροχος (Υπάρχει ένα C# μόνο λιμένας SQLiteΗ ανάπτυξη περιλαμβάνει κυριολεκτικά μια αναφορά και τη δημιουργία ενός αρχείου... τώρα έχει έναν αρκετά σοβαρό περιορισμό τον οποίο θα καλύψω αργότερα:)
Οι επιλογές μου ήταν nHibernate ή EF...υπάρχει υποστήριξη για SQLite και στις δύο αυτές πλατφόρμες.
Τώρα, έχω χρησιμοποιήσει nHibernate για ένα πρόσφατο σχέδιο (αποποίηση ευθύνης, Έφυγα αρκετά νωρίς στο έργο:) και ένα από τα πράγματα που με ενόχλησε ήταν ο μεγάλος αριθμός των εξαρτήσεων, και η φαινομενικά εύθραυστη σε εκδόσεις αυτών των εξαρτήσεων. Για μια εφαρμογή desktop χαμηλής πρόσκρουσης, αυτό ήταν ακριβώς πάρα πολύ μεγάλο ένα αποτύπωμα και πάρα πολύ μεγάλο ένα ρίσκο.
Αυτό μου άφησε μια επιλογήΠλαίσιο οντότηταςΤώρα, αυτό δεν ήταν μια εύκολη επιλογή... Έχω χρησιμοποιήσει EF στο παρελθόν (βρώμικο με αυτό) και βρήκα μια επώδυνη εμπειρία... (άλλοι έχουν πάρα πολύ που οδήγησε σε αυτόΤώρα, δεν πρόκειται να μπω στην πολιτική πίσω από την επιλογή MS για να ωθήσει EF (και ναι, ήταν μια πολιτική επιλογή), αλλά V1 είναι αρκετά buggy, και για μένα τουλάχιστον αρκετά αφύσικο. Αυτό οδήγησε σε δεύτερη μάθηση μου opportunity...Master πλαίσιο οντότητας! Καθώς γράφω αυτό, είναι κυριολεκτικά 2 ώρες από τότε που εργάστηκε μου τελευταίο κομμάτι του πόνου EF...που οδήγησε σε αυτό το μοντέλο:
Φαίνεται απλό...λοιπόν, ναι... αλλά μου πήρε AGES για να φτάσετε σε αυτό...Ξέρω SQL πραγματικά καλά, C# Πραγματικά καλά, αλλά το μείγμα των σκοτεινών έννοιες, κακή τεκμηρίωση και επιπόλαια μηνύματα σφάλματος κάνει την εργασία / μάθηση EF πολύ δύσκολο! EF 4 (σε VS 2010 / .NET 4) βελτιώνει αυτό...και δεν μπορεί να έρθει αρκετά σύντομα!
Οπότε, αυτό δούλεψε ελπίζω)
Έτσι, ανέφερα προηγουμένως ότι το SQLite έχει ένα κομμάτι ενός ζητήματος...δεν του αρέσει πραγματικά το concurrency. Λοιπόν, αυτό είναι μια υπερβολή...είναι ένα πολυ-διαβάσιμο, ενιαίο σύστημα γραφής. Μόλις το συνειδητοποιήσετε είναι ένα θέμα που είναι αρκετά εύκολο να λυθεί. Χρησιμοποιώ αυτό το μοτίβο:
public static void AddRange(IEnumerable<UploadFile> files)
{
using (PixEntities ent = new PixEntities())
{
using (new ReadLock(entityLock))
{
using (new WriteLock(entityLock))
{
foreach (var file in files)
{
ent.AddToUploadFiles(file);
ent.SaveChanges();
}
}
}
}
}
Όπως είδατε, αυτό έχει δύο κύρια στοιχείαΧρησιμοποιώ ένα ReadLock (όπου κανονικά θα διάβαζα * από* το DB, σε αυτή την περίπτωση umm, I dont :)) Στη συνέχεια, χρησιμοποιώ ένα WriteLock για να τυλίξει κάθε λειτουργία όπου γράφω στο DB. Ο παρατηρητής θα παρατηρήσει επίσης τι μοιάζει με ένα bug...Κάνω ent.SaveChages() για κάθε λειτουργία του βρόχου...αυτό είναι ένα άλλο SQLite quirk, δεν υποστηρίζει ενημερώσεις παρτίδα...και δίνει ένα περίεργο λάθος σχετικά με τις κλειδαριές αρχείων αν προσπαθήσετε:)
Το μικρό μάθημα κλειδώματος που χρησιμοποιώ είναι από Ορίστε., αλλά έχω θέσει κάτω για τη χρήση σας Τέλος πάντων, αυτό είναι ένα από τα πολλά μέρη της εμπειρίας μου στην εργασία σε κοζβεκανικήμείνετε συντονισμένοι!
public static class Locks
{
public static void GetReadLock(ReaderWriterLockSlim locks)
{
bool lockAcquired = false;
while (!lockAcquired)
lockAcquired = locks.TryEnterUpgradeableReadLock(1);
}
public static void GetReadOnlyLock(ReaderWriterLockSlim locks)
{
bool lockAcquired = false;
while (!lockAcquired)
lockAcquired = locks.TryEnterReadLock(1);
}
public static void GetWriteLock(ReaderWriterLockSlim locks)
{
bool lockAcquired = false;
while (!lockAcquired)
lockAcquired = locks.TryEnterWriteLock(1);
}
public static void ReleaseReadOnlyLock(ReaderWriterLockSlim locks)
{
if (locks.IsReadLockHeld)
locks.ExitReadLock();
}
public static void ReleaseReadLock(ReaderWriterLockSlim locks)
{
if (locks.IsUpgradeableReadLockHeld)
locks.ExitUpgradeableReadLock();
}
public static void ReleaseWriteLock(ReaderWriterLockSlim locks)
{
if (locks.IsWriteLockHeld)
locks.ExitWriteLock();
}
public static void ReleaseLock(ReaderWriterLockSlim locks)
{
ReleaseWriteLock(locks);
ReleaseReadLock(locks);
ReleaseReadOnlyLock(locks);
}
public static ReaderWriterLockSlim GetLockInstance()
{
return GetLockInstance(LockRecursionPolicy.SupportsRecursion);
}
public static ReaderWriterLockSlim GetLockInstance(LockRecursionPolicy recursionPolicy)
{
return new ReaderWriterLockSlim(recursionPolicy);
}
}
public abstract class BaseLock : IDisposable
{
protected ReaderWriterLockSlim _Locks;
public BaseLock(ReaderWriterLockSlim locks)
{
_Locks = locks;
}
public abstract void Dispose();
}
public class ReadLock : BaseLock
{
public ReadLock(ReaderWriterLockSlim locks)
: base(locks)
{
Locks.GetReadLock(this._Locks);
}
public override void Dispose()
{
Locks.ReleaseReadLock(this._Locks);
}
}
public class ReadOnlyLock : BaseLock
{
public ReadOnlyLock(ReaderWriterLockSlim locks)
: base(locks)
{
Locks.GetReadOnlyLock(this._Locks);
}
public override void Dispose()
{
Locks.ReleaseReadOnlyLock(this._Locks);
}
}
public class WriteLock : BaseLock
{
public WriteLock(ReaderWriterLockSlim locks)
: base(locks)
{
Locks.GetWriteLock(this._Locks);
}
public override void Dispose()
{
Locks.ReleaseWriteLock(this._Locks);
}
}