WPF per il ragazzo Web una storia di dolore, disperazione e apprendimento
Beh, OK I.OK I.O. sopra drammatizzare un po ':) Come si può aver notato dal mio Ultimo post Ho iniziato a lavorare di alcune cose che è al di fuori della mia zona di comfort. Recentemente sono entrato in una società chiamata cozwecano, lavorando per un tizio di nome @RobertTheGrey, il primo progetto su cui stiamo lavorando è un sito di vendita di foto... si sa un po 'come Getty Images / Smugmug / Flickr tranne per le vendite dirette fotografo-cliente. sviluppatore a tempo pieno (fino ad ora!) Ho avuto il compito di costruire la maggior parte delle applicazioni (sia web, desktop, ecc...) di cui abbiamo bisogno perché il business funzioni.
Ovviamente uno dei bit più critici per un sito di e-commerce è roba da vendere! Nel nostro caso questo è uno stock di fotografie di alta qualità davvero scattate da fotografi professionisti in tutto il mondo. Prima domanda; come facciamo a ottenere queste immagini sul sito, e come facciamo a lasciare che i fotografi aggiungere informazioni alle loro fotografie per lasciarli essere trovato da persone che vogliono comprarli? Beh, che si tratta del primo problema che è stato chiesto di risolvere!
Quando si lavora sui requisiti, è diventato rapidamente chiaro che questo doveva essere un'applicazione desktop, doveva consentire le operazioni su fotografie multi-megabyte, necessario per consentire l'accesso offline. Specialmente durante il caricamento iniziale, ci 100-100s di migliaia di immagini ottenere lavorato su ... costringendo questo per essere online aggiunge un brutto lag e rende il compito un dolore nel culo per completare. Soprattutto, doveva essere affidabile!
Ora, nella mia testa dev questo ha portato a una serie di decisioni sulle caratteristiche che l'applicazione avrebbe bisogno di supportare:
- UI intuitiva e reattiva; gli utenti non sono necessariamente esperti di computer, hanno bisogno di utilizzare metafore UI evidenti e rendono abbastanza veloce per essere piacevole da usare.
- Ha bisogno di fare qualche elaborazione di immagini offline; abbiamo intenzione di ospitare questo sul cloud.... tempo CPU costa denaro:)
- Dovrebbe funzionare fuori rete
- Ha bisogno di essere costruito intorno al caricamento affidabile di file multi-meg (tracce potenzialmente piccole larghezze di connessione).
Quindi, questi sono i bisogni di base tecnicamente questo mi porta ad alcune scelte tecnologiche:
- Multi-threading / concurrency (come con qualsiasi reale app desktop) è la chiave come è una decente tecnologia Desktop
- Vedi sopra, abbiamo bisogno di guardare per i nuovi file in arrivo, rendere le miniature in modo trasparente ecc
- Quindi, ha bisogno di una sorta di persistenza ... insieme ad altri requisiti app non-core (ad esempio il tag) abbiamo probabilmente bisogno di un po ', leggero DB.
- Necessita di upload basati sul blocco di plugin con riprova / verifica degli elementi caricati
Per fortuna sono stato un dev per un bel po 'e aveva almeno qualche idea da dove iniziare la sfida più grande sarebbe l'apprendimento!
In primo luogo, l'ovvia piattaforma desktop de jour è WPF ... questo è in sostanza l'attuale sostituzione per WinForms, è molto personalizzabile, ma ha una curva di apprendimento un po 'infame ripida. libri il WPF, seguito tutte le presentazioni PDC, podcast ecc...sul tema. Dopo 3 settimane mi sono finalmente trovato a suo agio con esso...
In secondo luogo, avevo bisogno di un DB... necessario per essere leggero, necessario per supportare LINQ (cosa, mi piace LINQ:)), necessario per supportare una sorta di tecnologia ORM.
La scelta più ovvia è stata: SQL Server Compact Edition, una sorta di ritaglio del server SQL che è grande per le applicazioni desktop ... tuttavia ... Non riuscivo a farlo funzionare! Si scopre che è molto sensibile alle versioni di vari componenti installati . Questa era una luce rossa per me ... Avevo bisogno che il DB per essere il più stand-alone possibile ! Che ha lasciato una scelta ovvia:
SQLite, dal sito:
La libreria è una libreria di software che implementa un self-contained, senza server, zero-configurazione, transazionale SQL database engine. SQLite è il la maggior parte dei casi di impiego SQL database engine nel mondo. Il codice sorgente per SQLite è nel pubblico dominio.’
Perfetto! Per il supporto .NET, ho usato il SQLite ADO.NET provider (c'è un C# solo porta di SQLite, ma è più lento e non necessario per il mio scenario. Il deployment è letteralmente compreso un riferimento e la creazione di un file ... ora ha una limitazione abbastanza grave che io coprire più tardi:)
Le mie scelte ORM sono state nibernato o EF... c'è supporto per SQLite in entrambe le piattaforme.
Ora, ho usato nHibernate per un progetto recente (disclaimer, ho lasciato abbastanza presto nel progetto:)) e una delle cose che mi ha infastidito era il gran numero di dipendenze, e l'apparente fragilità tra le versioni di queste dipendenze. Per un basso impatto app desktop, questo era solo troppo grande un'impronta e troppo grande un rischio. Così ho dovuto contare fuori.
Questo mi ha lasciato una sola sceltaQuadro dell'entità. Ora, questa non è stata una scelta facile ... Ho usato EF in passato (appiccicato con esso) e trovato un'esperienza dolorosa ... [altri hanno troppo che ha portato a questo). Ora, non entrare nella politica dietro la scelta degli Stati membri per spingere EF (e sì, era una scelta politica), ma V1 è abbastanza buggy, e per me almeno abbastanza innaturale. Ciò ha portato alla mia seconda opportunità di apprendimento ... Master Entity Framework! Mentre scrivo questo, è letteralmente 2 ore da quando ho elaborato il mio ultimo pezzo di dolore EF ... che ha portato a questo modello:
Sembra semplice... beh, sì... ma mi ha portato AGES per arrivare a questo... So SQL davvero bene, C# Davvero bene, ma il mix di concetti oscuri, scarsa documentazione e messaggi di errore offuscati rende il lavoro / apprendimento EF modo troppo difficile! EF 4 (in VS 2010 / .NET 4) migliora questo...e può venire presto!
Quindi, che ha funzionatoSpero:)
Così, ho accennato in precedenza che SQLite ha un po 'di un problema... esso non piace molto concurrency. Beh, che è un overstatement... è un sistema di scrittura singola multi-letto. Una volta che vi rendete conto che è un problema che è abbastanza facile da risolvere. Uso questo modello:
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();
}
}
}
}
}
Come avete visto, questo ha due elementi principali uso un ReadLock (dove normalmente leggerei *da* il DB, in questo caso umm, I don-tit:)) quindi uso un WriteLock per avvolgere qualsiasi operazione in cui scrivo al DB. L'osservatore noterà anche quello che sembra un bug...Io ent.SaveChages() per ogni iterazione del ciclo...questa è un'altra caratteristica SQLite, non supporta gli aggiornamenti batch...e dà un errore strano circa i blocchi di file se si tenta di:)
La piccola classe di bloccaggio che uso è da qui, ma lo ho messo qui sotto per il vostro uso in ogni caso, questa è una parte di diverse parti delle mie esperienze nel lavorare su cozwecanoRestate sintonizzati!
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);
}
}