Nonostante in Silverlight 2.0 sia completamente disponibile un linguaggio di programmazione come C#, resistono ancora alcune casistiche in cui occorre fare ricorso all'uso di Javascript per interagire con la pagina. Per questo esiste la classe HtmlPage che funge da entry point della nostra pagina HTML e che facilmente ci consente di interagire con essa dal codice managed.

Esiste però anche la remota possibilità (non poi così remota in effetti...) che avendo del codice Javascript si renda necessario accedere ad un metodo managed esposto dal plugin. A me ad esempio è capitato di dover intercettare l'uso della rotellina del mouse che Silverlight non consente di controllare. Per questo mi sono scritto il codice Javascript e al verificarsi di un evento chiamo un metodo esposto da uno dei miei UserControl. I passi per raggiungere questo scopo sono essenzialmente due:

A) Registrare il controllo che contiene il metodo da esporre con il metodo RegisterScriptableObject()

   1: public MapControl()
   2: {
   3:     // Required to initialize variables
   4:     InitializeComponent();
   5:  
   6:     HtmlPage.RegisterScriptableObject("MapControl", this);
   7: }

Questo passaggio farà in modo che il plugin esponga all'interno della proprietà content una proprietà denominata "MapControl" che contiene il riferimento al nostro UserControl. Naturalmente possiamo esporre sia un singolo controllo che la pagina stessa.

B) Decorare i membri del controllo che devono essere visibili al Javascript con l'attributo [ScriptableMember]

   1: [ScriptableMember]
   2: public string HandleMouseWheel(double mouseX, double mouseY, int wheelDelta)
   3: {
   4:     //... do your work here
   5: }

A questo punto da javascript sarà molto semplice trovare una referenza al plugin, conoscendo il nome del controllo, e chiamare il metodo esposto:

   1: var plugin = document.getElementById("pluginId");
   2:  
   3: plugin.content.MapControl.HandleMouseWheel(123, 456, 7);

Semplice ed efficace. L'uso di un attributo per marcare le proprietà e i metodi è da tenere in seria considerazione. Infatti in questo modo metteremo al sicuro il nostro codice da chiamate "indiscrete" a metodi che non hanno alcuno scopo valido.


Il fatto di avere a che fare con un runtime che permette di eseguire CLR non significa che non serva mai ricorrere all'uso di Javascript. M'è capitato ormai svariate volte di dovermi agganciare ad un evento del DOM HTML  o di dover in qualche modo interagire con altri oggetti nella pagina. In Silverlight 2.0 è possibile fare ciò per mezzo dell'oggetto HtmlPage che espone un punto di accesso al contenuto della pagina. Con esso possiamo ad esempio collegarci ad eventi del dom (AttachEvent) o modificare elementi e attributi o ancora invocare metodi Javascript.

A me è capitato ad esempio di dover interagire con uno ScriptControl che normalmente viene registrato nella pagina dalla Ajax Library ed è accessibile mediante il metodo $find(). Per invocare il medesimo metodo da Silverlight occorre usare la seguente sintassi:

   1: ScriptObject map = 
   2:     (ScriptObject)HtmlPage.Window.Invoke("$find", "map");

Ci sono casi in cui non è disponibile uno shortcut definito come nel caso di $find. Ad esempio se dobbiamo usare il metodo Sys.UI.DomElement.getBounds() le cose si complicano notevolmente:

   1: ((ScriptObject)((ScriptObject)((ScriptObject)((ScriptObject)
   2:         HtmlPage.Window
   3:             .GetProperty("Sys"))
   4:             .GetProperty("UI"))
   5:             .GetProperty("DomElement"))
   6:             .Invoke("getBounds", HtmlPage.Document.Body));

In sostanza dobbiamo cercare ricorsivamente le proprietà e alla fine invocare il metodo. COn un po' di codice è possibile scrivere dei metodi che aiutino a semplificare questo lavoro:

   1: /// <summary>
   2: /// Invokes the ex.
   3: /// </summary>
   4: /// <param name="so">The so.</param>
   5: /// <param name="methodPath">The method path.</param>
   6: /// <param name="args">The args.</param>
   7: /// <returns></returns>
   8: public static object InvokeEx(this ScriptObject so, string methodPath, params object [] args)
   9: {
  10:     IEnumerable<string> parts = Parse(methodPath);
  11:  
  12:     foreach (string part in parts)
  13:     {
  14:         if (part != parts.Last())
  15:             so = (ScriptObject)so.GetProperty(part);
  16:         else
  17:             return so.Invoke(part, args);
  18:     }
  19:  
  20:     throw new ArgumentException("methodPath");
  21: }
  22:  
  23: /// <summary>
  24: /// Gets the property ex.
  25: /// </summary>
  26: /// <param name="so">The so.</param>
  27: /// <param name="propertyPath">The property path.</param>
  28: /// <returns></returns>
  29: public static ScriptObject GetPropertyEx(this ScriptObject so, string propertyPath)
  30: {
  31:     IEnumerable<string> parts = Parse(propertyPath);
  32:  
  33:     foreach (string part in parts)
  34:         so = (ScriptObject)so.GetProperty(part);
  35:  
  36:     return so;
  37: }
  38:  
  39: /// <summary>
  40: /// Parses the specified path.
  41: /// </summary>
  42: /// <param name="path">The path.</param>
  43: /// <returns></returns>
  44: private static IEnumerable<string> Parse(string path)
  45: {
  46:     Match match = Regex.Match(path, @"(?<part>[^\.]*)(\.(?<part>[^\.]*))*");
  47:  
  48:     if (match.Success)
  52:         return from Capture cp in match.Groups["part"].Captures
  53:                select cp.Value;
  55:  
  56:     throw new ApplicationException(string.Format("Unable to parse path '{0}'}", path));
  57: }

Si tratta di un paio di Extension Method che estendono la classe ScriptObject e consentono di specificare il metodo o la proprietà con la classica notazione puntata. Il metodo principale, Parse, fa uso di una regular expression per verificare la validità della stringa passata ed estrarre la varie porzioni. Ecco come usare il nuovo metodo:

   1: ScriptObject bounds = 
   2:     (ScriptObject)HtmlPage.Window.InvokeEx("Sys.UI.DomElement.getBounds", HtmlPage.Document.Body);
Technorati Tag: ,,

Ho già parlato un po' di tempo fa di con riferimento ad ASP.NET. Stasera voglio tornarci su per parlare della medesima caratteristica di Silverlight 2.0. L'event bubbling è quella particolare caratteristica che fa in modo che un evento sollevato da un elemento di propaghi agli elementi immediatamente superiori e così via fino a raggiungere la radice della gerarchia. Il bubbling può essere molto utile ma per poterlo sfruttare al meglio è necessario essere in grado di interrompere la propagazione dell'evento in un particolare punto.

In Silverlight 1.0 gestire l'event bubbling è problematico perchè non esiste un metodo "istituzionale" per per interrompere la propagazione dell'evento ma per farlo occorre appoggiarsi a delle variabili globali da valorizzare opportunamente. In Silverlight 2.0 invece è stato introdotto un argomento Handled che opportunamente valorizzato e gestito consente di gestire con facilità questo problema.

   1: private void child_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
   2: {
   3:     // handle event
   4:  
   5:     e.Handled = true;
   6: }
   7:  
   8: //........
   9:  
  10: private void father_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
  11: {
  12:     if (!e.Handled)
  13:     {
  14:         // event was not handled
  15:     }
  16: }

Naturalmente questa proprietà si applica agli eventi che subiscono il bubbling. In particolare gli eventi che lo gestiscono sono tutti quelli relativi il mouse.


Uno delle cose che mancano in Silverlight, sin dalla versione 1.0, è la possibilità di gestire l'evento DoubleClick del Mouse. Pur se Silverlight è ricco di eventi che riguardano il mouse è carente secondo due punti di vista. Il tasto destro del mouse non è gestibile, ma questo lo si deve alla presenza di un menù di contesto di configurazione del plugin. Ma l'assenza di doppio click è davvero singolare e talvolta fastidiosa. Perciò, ora che è uscita la beta 1 di Silverlight 2.0, e preso atto che la mancanza è ancora presente ho pensato di creare una classina che mi venga in aiuto quando ho la necessità di distinguere tra singolo e doppio click.

La questione è piuttosto semplice. Abbiamo a disposizione sono l'evento MouseLeftButtonUp e da esso dobbiamo discriminare ed è piuttosto ovvio che la variabile da tenere in considerazione sia il tempo. Potremmo immaginare che quando arriva un primo evento dobbiamo attendere un certo numero di millisecondi (diciamo 300 circa). Se entro questo tempo arriva un secondo evento siamo in presenza di un doppio click mentre al loro scadere possiamo considerare di avere a che fare con un single click.

Gestire questa condizione comporta però un bel po' di lavoro e soprattutto saremmo costretti a scrivere questo flusso svariate volte, almeno una volta per ciascun elemento per il quale abbiamo la necessità di gestire il doppio click. Perciò da buoni programmatori, convinti che il nostro lavoro sia di scrivere poco codice, dobbiamo realizzare una classe che ci aiuti ogni volta che ne abbiamo bisogno.

La classe che ho realizzato e che trovate in allegato a questo post prende in carico tutti gli eventi di click che arrivano da una determinata sorgente e applica la logica cui ho accennato poco fa. Poi. alla scadenza dei 300 millisecondi si incarica di sollevare a sua volta un altro evento corrispondente al singolo o doppio click. Purtroppo, dato che per ottenere l'eventuale secondo click dobbiamo restituire immediatamente il controllo al chiamante, siamo costretti a lanciare un thread che si incarichi di attendere il timeout e lanciare l'evento Click (quello singolo) nel caso in cui non arrivi più nulla.  Ecco il corpo principale del MouseClickManager:

   1: /// <summary>
   2: /// Handles the click.
   3: /// </summary>
   4: /// <param name="sender">The sender.</param>
   5: /// <param name="e">The <see cref="System.Windows.Input.MouseButtonEventArgs"/> instance containing the event data.</param>
   6: public void HandleClick(object sender, MouseButtonEventArgs e)
   7: {
   8:     lock(this)
   9:     {
  10:         if (this.Clicked)
  11:         {
  12:             this.Clicked = false;
  13:             OnDoubleClick(sender, e);
  14:         }
  15:         else
  16:         {
  17:             this.Clicked = true;
  18:             ParameterizedThreadStart threadStart = new ParameterizedThreadStart(ResetThread);
  19:             Thread thread = new Thread(threadStart);
  20:             thread.Start(e);
  21:         }
  22:     }
  23: }
  24:  
  25: /// <summary>
  26: /// Resets the clicked flag after timeout.
  27: /// </summary>
  28: /// <param name="state">The state.</param>
  29: private void ResetThread(object state)
  30: {
  31:     Thread.Sleep(this.Timeout);
  32:  
  33:     lock (this)
  34:     {
  35:         if (this.Clicked)
  36:         {
  37:             this.Clicked = false;
  38:             OnClick(this, (MouseButtonEventArgs)state);
  39:         }
  40:     }
  41: }

Nel metodo HandleClick dapprima acquisiamo un lock. E' una precauzione necessaria perchè come vedremo il flag Clicked diventa una risorsa condivisa fra due thread diversi e quindi il lock ci serve ad evitare problemi di concorrenza su di essa. A questo punto i casi sono due: se troviamo Clicked settato a false significa che siamo in presenza del primo click e quindi dopo aver settato il flag a true avviamo un thread secondario. Se invece Clicked è già settato significa che è arrivato il secondo click perciò si azzera il flag e poi si provvede a sollevare l'evento b. Nel thread secondario innanzitutto si attendono i millisecondi configurati nella proprietà Timeout e al loro scadere se il flag è ancora settato lo si azzera e si solleva l'evento Click. Fermatevi un attimo a pensarci su se la cosa non è così chiara... comunque vi assicuro che funziona.

Ora c'è un problema da gestire. Dato che l'evento Click viene sollevato da un thread secondario quando questo arriva all'interfaccia ci troveremo nella spiacevole condizione di non essere sincronizzati con il thread principale che appunto detiene l'interfaccia stessa. Questo significa che se tutto va bene... non funzionerà nulla :) Abbiamo bisogno di fare il marshaling del contesto per rendere l'evento Click fruibile dall'interfaccia. Questo è un problema comune all'ambiente Windows Forms che si ripresenta anche all'interno del plugin di Silverlight. Ecco il corpo del metodo OnClick deputato a sollevare l'evento:

   1: /// <summary>
   2: /// Called when click occure.
   3: /// </summary>
   4: /// <param name="sender">The sender.</param>
   5: /// <param name="e">The <see cref="System.Windows.Input.MouseButtonEventArgs"/> instance containing the event data.</param>
   6: private void OnClick(object sender, MouseButtonEventArgs e)
   7: {
   8:     MouseButtonEventHandler handler = Click;
   9:  
  10:     if (handler != null)
  11:         this.Control.Dispatcher.BeginInvoke(handler, sender, e);
  12: }

L'oggetto rappresentato dalla proprietà Control è l'istanza del controllo che dovrà ricevere l'evento. Questo deve essere passato al costruttore della classe assieme al timeout desiderato. Il dispatcher si occuperà per noi di gestire il marshaling del contesto e il gioco è fatto.

Usare la classe MouseClickManager è semplice. Se ne crea un'istanza passandole un riferimento al controllo che la contiene e un timeout. Poi si agganciano i due eventi DoubleClick e Click e infine si fa in modo che all'interno dell'evento MouseLeftButtonUp venga chiamato il metodo HandleClick e si passano ad esso gli argomenti sender e args.

La scelta del timeout è soggettiva. Sarebbe opportuno poter configurare la soglia in base all'abilità dell'utente. Comunque da qualche esperimento che ho fatto si evince che 200 millisecondi sono troppo pochi mentre 400 sono troppi e si inizia ad apprezzare il ritardo dell'evento Click rispetto al clieck vero e proprio.

Download: http://blog.boschin.it/download/mouseclickmanager.zip (1 KB)

 

Technorati Tag: ,

Visto che ci riferiamo ad un plugin del browser, parlare di thread e di marshaling può sembrare una pura utopia. Invece se provate a guardare le classi esposte da Silverlight 2.0 noterete che il namespace System.Threading è implementato quasi al 100%. Thread, Timer, ThreadPool. Tutto e molto di più è a nostra disposizione per lanciare dei thread che si occupino di fare del lavoro in parallelo. Questo però apre la strada ad un problema classico, che tipicamente si incontra nelle Windows Forms. Se ad esempio lanciate un thread in una Form, e questo thread tenta di aggiornare una parte dell'interfaccia (ad esempio scrivendo in una TextBox) ci troveremmo in presenza di un problema dovuto al fatto che il thread secondario e l'interfaccia sono in due thread separati e ogni accesso che tenti di superare la barriera che li separa è destinato a fallire in modo casuale. In effetti il risultato dui questa operazione è indeterminato tanto che il più delle volte fallirà ma talvolta potrebbe anche andare a buon fine.

Per risolvere questo problema, che in Silverlight è analogo è necessario usare l'oggetto Dispatcher. Questo oggetto è esposto dalla classe Control e di conseguenza da qualunque controllo di Silverlight. Il Dispatcher di Silverlight espone il metodo BeginInvoke() che è in grado di chiamare un delegate effettuando quello che va sotto il nome di Marshaling. In buona sostanza quello che avviene è che viene immesso un messaggio nella coda del thread principale il quale alla ricezione si occuperà di chiamare il delegate. Ecco come fare:

   1: /// <summary>
   2: /// Called when click occure.
   3: /// </summary>
   4: /// <param name="sender">The sender.</param>
   5: /// <param name="e">The <see cref="System.Windows.Input.MouseButtonEventArgs"/> instance containing the event data.</param>
   6: private void OnClick(object sender, MouseButtonEventArgs e)
   7: {
   8:     MouseButtonEventHandler handler = Click;
   9:  
  10:     if (handler != null)
  11:         this.Control.Dispatcher.BeginInvoke(handler, sender, e);
  12: }

Il metodo BeginInvoke() richiede in ingresso il delegate da chiamare e qualora di tratti di un evento consente di specificare gli argomenti che esso dovrà ricevere. Si tratta di una param list perciò il delegate o il callback potrà ricevere un numero indefinito di parametri. Il controllo da usare per trovare una referenza al Dispatcher è il destinatario dell'evento che dobbiamo sollevare. Un buon candidato potrebbe essere lo UserControl.


Anche Silverlight 2.0 come la versione precedente ha la possibilità di attivare la modalità Windowsless cioè quella particolare modalità per cui il plugin diventa trasparente laddove non vi siano aree opache. A prescindere dal fatto che l'uso di questa modalità va ben ponderato perchè la sua attivazione comporta un onere molto maggiore in termini di elaborazione, i risultati che si possono ottenere sono davvero molto "impressive" e le applicazioni si sprecano...

Ho trovato qualche difficicoltà a riprodurre il medesimo comportamento, tuttavia ho scoperto che l'unico problema è quello di trovare la giusta "taratura" delle proprietà. Ecco come impostare il plugin con il nuovo controllo ASP.NET:

   1: <asp:Silverlight ID="overlay" runat="server" 
   2:                  Source="~/ClientBin/SilverlightApplication1.xap" 
   3:                  Windowless="true" 
   4:                  PluginBackground="#00000000" 
   5:                  Version="2.0" 
   6:                  Width="100%" Height="100%"
   7:                  style="z-index:200;" />

Il "segreto" è quello di impostare il colore del background del plugin a trasparente usando un alpha channel con la proprietà PluginBackground. Diversamente anche se la proprietà Windowless è impostata a True, il colore opaco dello sfondo del plugin renderà vano ogni tentativo. Il colore #00000000 in prima analisi potrebbe apparire come un nero, ma i primi due zeri che sono in più rispetto alla normale sintassi HTML corrispondono al grado di opacità e 00 ovviamente significa "trasparente". In più impostare uno z-index serve a "sollevare" il plugin e portaro in primo piano rispetto al resto. A voi eventualmente la decisione di applicare stili per il posizionamento assoluto qualora abbiate la necessità di sovrapporlo a delle parti di HTML.

- English version (automatic translation)


La domanda non è oziosa e la risposta non è "nulla" ma nemmeno "tutto". La licenza che accompagna Silverlight 2.0 è di tipo Go-Live e questo significa che è possibile pubblicare i propri lavori ma solo entro certi limiti. I limiti riportati nel documento di licenza sono i seguenti:

  • you are fully responsible for any and all damages that may result due to any failure of the software;
  • you must notify all recipients that your Silverlight applications rely on pre-release, time-limited, unsupported software that may not operate correctly;
  • you may not make any representation, warranty or promise on behalf of Microsoft or with respect to the software, or its performance;
  • you may not make available any Silverlight applications that conduct e-commerce transactions, that collect personally identifiable information or confidential data, or which are for hazardous environments that require fail safe controls; and
  • you may not offer Silverlight applications on a commercial basis, such as under a paid subscription or on ad-funded web pages.

Quindi se fate qualche eseperimento interessante potete tranquillamente pubblicarlo, a patto che rispettiate i punti elencati. E' comunque escluso il lucro... almeno è per ora.


Oggi sono finalmente riuscito a venire a capo di un problema che da tempo mi affliggeva. Nella mia applicazione Silverlight, apparentemente in modo causale la barra di controllo laterale di tanto in tanto si spostava nel bel mezzo della pagina... nulla di così grave, bastava fare il resize della finestra e tutto tornava a posto, ma ovviamente il comportamento è del tutto inaccettabile. Oggi finalmente ho avuto il tempo di capire l'origine del malfunzionamento che si è rivelato originato da un probabile bug imputabile a Silverlight.

L'origine del problema è la seguente: Quando avete un host, posizionato sulla pagina al 100% in modo da riempire in modo dinamico un'area della pagina (o tutta), pare che l'apertura di un nuovo tab di Internet Explorer 7.0 metta un po' in crisi il plugin. Quello che avviene pochi istanti dopo essere passati al nuovo tab è che l'host solleva due eventi OnResize. Il primo è corretto anche se non necessario, ma il secondo (immediatamente consecutivo) invece arriva portando con se delle misure fasulle dell'host pari circa al 50% della misura reale.

L'unico modo che ho trovato di gestire questo errore è di confrontare le misure che mi arrivano con una misura certa all'interno della pagina che mi confermi che il dato non è errato. Per farlo è praticamente obbligatorio usare la MicrosoftAjax Library:

   1: handleResize : function(sender, eventArgs)
   2: {
   3:     var bounds = Sys.UI.DomElement.getBounds($get('silverlightPlugInHost'));
   4:     
   5:     if (bounds.width != this.plugIn.content.actualWidth && 
   6:         bounds.height != this.plugIn.content.actualHeight)
   7:         return;
   8:     
   9:     // Do what you want with actualWidth & actualHeight
  10: }

L'obbligo nell'uso della MS Ajax Library deriva dal fatto che implementare in proprio la getBounds() in modo cross-browser è tutt'altro che una passeggiata. Quello che faccio in sostanza è di verificare le dimensioni date da actualWidth e actualHeight per mezzo del DIV che contiene l'host. Naturalmente questo dovrà avere le stesse dimensioni dell'host altrimenti non vale...

Qualcuno potrebbe dire che a questo punto varrebbe la pena di usare direttamente le dimensioni di DIV, ma considerate che io l'applicazione l'ho già fatta e così devo modificare il minimo indispensabile. E poi preferisco usare il metodo normale e gestire i casi eccezionali diversamente.


La è di quelle che fanno ben sperare. A quanto pare Nokia ha deciso di effettuare il porting di Silverlight sui propri telefoni della piattaforma S60 e S40. A giudicare quello che dice questo post durante la keynote del Mix sarà presentata una demo funzonante. Ecco il commento di :

This is an important relationship on so many levels. Working with Nokia means we are easily able to reach a huge number of mobile users, including customers of all S60 licensees.  This is a significant step in gaining broad acceptance for Silverlight and ensuring it is platform agnostic. This is critical since we want to make sure developers and designers don't have to constantly recreate the wheel and build different versions of applications and services for multiple operating systems, browsers and platforms," said S. Somasegar, Senior Vice President of Microsoft's Developer Division

Fonte: