Blog di LucaB

audio, video, disco

Un plugin per Windows Live Writer

Nel poco tempo a disposizione nell'ultima settimana (una motherboard si è rotta e la nuova si è rivelata difettosa giusto alla fine delle reinstallazioni e ripristino dati) ho provato ad arricchire questo blog con alcuni accessori, come snapshot per i link. Dopo essermi registrato su Technorati, inoltre, ho aggiunto ai post già scritti (tanto sono ancora pochi) i Technorati tags e ho voluto provare ad aggiungere a ciascuno di essi il Link Count Widget, ossia un'indicazione di quanti sono i blog che linkano quel post (per la cronaca, ancora nessuno fino ad ora Sad).

Per aggiungere tale feature su Community Server bisogna inserire in ogni post questo snippet in HTML:

<script src="http://embed.technorati.com/linkcount" type="text/javascript">
</script> <a class="tr-linkcount" href="http://technorati.com/search/{URL}">
View blog reactions</a>

In realtà la prima parte può essere messa una volta per tutte nell'header HTML delle pagine del blog, specificandolo nella Dashboard relativa al proprio account. Nella seconda parte, invece, bisogna sostituire a {URL} l'indirizzo completo del post in questione.

Il problema è che l'indirizzo del post non è noto fino a che il post non viene pubblicato, anche se è ricavato dal titolo dello stesso e dalla data di pubblicazione con un semplice algoritmo. Un metodo semplice per ovviare a questo inconveniente sarebbe di scrivere al posto di {URL} un'espressione di DataBind del tipo:

<%# BlogUrls.Instance().Post(WeblogControlUtility.Instance()
.GetCurrentWeblogPost(this)) %>

Purtroppo, per ovvi motivi di sicurezza, non è possibile inserire espressioni di DataBind in un post (sarebbero codificate e rese innocue). Pertanto, non avendo accesso alle impostazioni di administrator della piattaforma, l'unica scelta sembrava quella di una doppia pubblicazione, con l'inserzione manuale dello snippet e dell'URL solo in seconda battuta.

Una leggera semplificazione, però, si può ottenere grazie ad un semplice plugin per Live Writer, che sia in grado di automatizzare parte della procedura ed evitare di dover inviare due volte il post (suggerimento di Paperino).

Per farlo da qui ho scaricato da qui Windows Live Writer SDK, l'ho installato ed ho dato un'occhiata alla documentazione ed al plugin di esempio (HelloWorldPlugin), da cui ho capito che per risolvere il mio problema avrei dovuto scrivere un Simple Content Source Plugin. Come source per il content avrei dovuto un Insert Dialog.

I passi per la creazione di un Content Source Plugin dalle caratteristiche indicate sono i seguenti:

  1. creare un nuovo progetto Class Library in Visual Studio;
  2. aggiungere una reference all'assembly WindowsLive.Writer.Api (che si trova nella stessa cartella dell'eseguibile di Windows Live Writer, ad es: C:\Programmi\Windows Live Writer);
  3. creare una nuova classe, nel mio caso TLCWPlugin, derivata dalla classe ContentSource;
  4. aggiungere a questa classe gli attributi WriterPluginAttribute e InsertableContentSourceAttribute e scrivere l'override del metodo CreateContent.

Se si aggiunge alle opzioni di compilazione la nei post-build event command line

XCOPY /D /Y /R "$(TargetPath)" "C:\Programmi\Windows Live Writer\Plugins\"

allora la dll generata sarà copiata direttamente nella directory dei plugin di Live Writer per essere provata.

Il codice risultante è (in VB.NET):

Imports System
Imports System.Windows.Forms
Imports WindowsLive.Writer.Api

<WriterPlugin("ce9a848e-c786-4e54-a0e2-1abff354e396", 
"Technorati Link Count Widget", ImagePath:="Technorati.png", _
Description:="Insert Technorati Link Count Widget in your post.", _
PublisherUrl:="http://www.webis.it")> _ <InsertableContentSource("Technorati Link Count Widget", _
SidebarText:="Link Count Widget")> _ Public Class TLCWPlugin Inherits ContentSource Public Overrides Function CreateContent( _
ByVal dialogOwner As System.Windows.Forms.IWin32Window, _
ByRef newContent As String) As System.Windows.Forms.DialogResult Using InsertForm As TLCWInsertForm = New TLCWInsertForm Dim risultato As DialogResult = InsertForm.ShowDialog If risultato = DialogResult.OK Then newContent = InsertForm.Stringa End If Return risultato End Using End Function End Class

Come si può vedere nell'attributo WriterPluginAttribute, oltre a id e nome, è possibile specificare anche altre informazioni, quali l'icona del plugin (una immagine embedded di dimensioni 20x18). Nell'attributo InsertableContentSource, inoltre, si può specificare un testo per la Sidebar diverso da quello del menù (c'è meno spazio).

Nel corpo del metodo CreateContent, invece, il codice non fa altro che aprire una finestra di dialogo con la form TLCWInsertForm, in cui sarà fatto tutto il lavoro.

screenshot del plugin in azione

Se la finestra è chiusa con DialogResult.OK (Insert), allora verrà inserito il nuovo contenuto, prelevandolo dalla proprietà pubblica Stringa della classe TLCWInsertForm.

Il codice per la proprietà è semplicemente:

    Private _Stringa As String

    Public Property Stringa() As String
        Get
            Return _Stringa
        End Get
        Set(ByVal Value As String)
            _Stringa = Value
        End Set
    End Property

(scritto ancora più velocemente grazie a questa macro di Francesco Balena, leggermente modificata, che trasforma field in proprietà)

Al caricamento della form viene eseguito il codice seguente, che carica (se presenti) le impostazioni memorizzate per il plugin, ossia l'URL del blog[1] e la presenza nell'header HTML del tag script:

Private Sub TLCWInsertForm_Load(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles Me.Load Me.DateTimePicker1.Value = Date.Today Me.Stringa = "" Me.LoadSettings() Me.TextBoxBlogUrl.Text = Me._blogUrl Me.CheckBoxJsInHeader.Checked = Me._checked Me.LabelNote.Visible = Not Me.CheckBoxJsInHeader.Checked End Sub
Private Sub LoadSettings()
    Dim fs As FileStream = Nothing
    Dim rd As StreamReader
    Me._blogUrl = Me._defaultBlogUrl
    Me._checked = Me._defaultChecked
    Try
        fs = New FileStream(Me._configPath & Path.DirectorySeparatorChar & _
Me._configFileName, FileMode.Open) rd = New StreamReader(fs) Me._blogUrl = rd.ReadLine().Trim If CBool(rd.ReadLine) = True Then Me._checked = True End If Catch ex As Exception Finally If fs IsNot Nothing Then fs.Close() End If End Try End Sub

Invece alla pressione di Insert, vengono effettuati controlli sulla presenza dei campi obbligatori, viene calcolato l'url in base alla data (di default è impostata quella di sistema) ed al titolo (attraverso la funzione UrlEncode), si imposta la proprietà Stringa e vengono salvate le impostazioni se sono cambiate.

Private Sub ButtonOK_Click(ByVal sender As Object, ByVal e As System.EventArgs) _
Handles ButtonOK.Click If DatiValidi() Then Dim url As String url = TextBoxBlogUrl.Text If Not url.EndsWith("/") Then url &= "/" End If url &= DateTimePicker1.Value.Year.ToString & "/" & _
DateTimePicker1.Value.Month.ToString("00") & "/" & _
DateTimePicker1.Value.Day.ToString("00") & "/" & _
UrlEncode(TextBoxPostTitle.Text, _
New Regex("([^A-Za-z0-9 ]+|\.| )", _
(RegexOptions.Singleline Or RegexOptions.Compiled)), "-"c, "_"c) & _
".aspx" Me.Stringa = String.Format(Me._technoratiString, url) If CheckBoxJsInHeader.Checked = False Then Me.Stringa = Me._technoratiJavascript & Me.Stringa End If If Not Me.SaveSettings() Then MsgBox("Error while saving your settings.") End If Else Me.DialogResult = Windows.Forms.DialogResult.None End If End Sub Private Function SaveSettings() Dim risultato As Boolean = True ' salva solo se le impostazioni sono cambiate If (Me._blogUrl <> TextBoxBlogUrl.Text) OrElse _
(Me._checked <> CheckBoxJsInHeader.Checked) Then Dim wr As StreamWriter = Nothing Try If Not Directory.Exists(Me._configPath) Then Directory.CreateDirectory(Me._configPath) End If wr = My.Computer.FileSystem.OpenTextFileWriter(Me._configPath & _
Path.DirectorySeparatorChar & Me._configFileName, False) wr.WriteLine(TextBoxBlogUrl.Text) wr.WriteLine(CheckBoxJsInHeader.Checked.ToString) Catch ex As Exception risultato = False Finally If wr IsNot Nothing Then wr.Close() End If End Try End If Return risultato End Function

Per quanto riguarda la funzione:

Private Function UrlEncode(ByVal titolo As String, _
ByVal pattern As Regex, _
ByVal spaceReplacement As Char, _
ByVal escapePrefix As Char) As String

è stato abbastanza semplice estrapolarla dal binario di Community Server, grazie a Reflector for .NET ed è disponibile insieme al codice completo qui.

Nota bene: per chi non potesse o non volesse mettere nell'header HTML il tag script, è presente anche una checkbox apposita, il cui valore rimane memorizzato per la volta successiva. Attenzione: questa feature deve essere utilizzata in modalità HTML Code, posizionando il cursore al di fuori di qualsiasi container HTML, altrimenti il tag script verrà semplicemente ignorato da Live Writer. Non sono riuscito, infatti, a trovare un modo per costringere il plugin ad aggiugere il content alla fine di tutto il testo (forse non esiste).

Il plugin è scaricabile liberamente da qui (TechnoratiLinkCountWidget.zip - 12KB), mentre il codice completo è qui (TLCWplugin.zip - 570KB)

View blog reactions


[1] L'URL del blog deve comprendere anche la subdirectory dove vengono archiviati gli articoli, ad esempio su dotnetside per il mio blog si avrebbe: http://www.dotnetside.org/blogs/lucab/archive
Un esercizio potrebbe essere di aggiungere alle opzioni del plugin un piccolo parser che costruisca l'URL in base ad un formato impostato dall'utente.