Technology Experience
.NET World

App Control Panel per UWP

Seguendo il gruppo Sviluppatori Windows Italia su Facebook, mi sono ritrovato nell’intento di voler ricreare una specie di Pannello di Controllo di Windows 10, applicando Adaptive UI per fare in modo che la visualizzazione cambi da griglia a lista, esattamente come fa il pannello di controllo nativo del sistema operativo. Perciò ho creato questo. Che ne dite?

Vedete come ridimensionando la finestra, la griglia si adatti in modo opportuno, e quando la dimensione della finestra scende al di sotto dei 600 pixel, la visualizzazione passa in modalità lista.

Passiamo alla parte tecnica. La visualizzazione a griglia è rappresentata dal seguente XAML:

   1: <GridView ItemsSource="{Binding Items}" VerticalAlignment="Center"

   2:     HorizontalAlignment="Center" x:Name="ControlPanelGrid">

   3:     <GridView.ItemsPanel>

   4:         <ItemsPanelTemplate>

   5:             <ItemsWrapGrid Orientation="Horizontal" />

   6:         </ItemsPanelTemplate>

   7:     </GridView.ItemsPanel>

   8:     <GridView.ItemTemplate>

   9:         <DataTemplate>

  10:             <Grid Width="200" Height="200" Padding="16" Margin="2">

  11:                 <Grid.RowDefinitions>

  12:                     <RowDefinition Height="Auto" />

  13:                     <RowDefinition Height="Auto" />

  14:                     <RowDefinition Height="Auto" />

  15:                 </Grid.RowDefinitions>

  16:                 <TextBlock Text="{Binding Icon}" Foreground="Olive" FontFamily="Segoe UI Symbol"

  17:                            FontSize="50" FontWeight="ExtraLight" TextAlignment="Center"

  18:                            TextWrapping="Wrap" Grid.Row="0" VerticalAlignment="Bottom" />

  19:                 <TextBlock Text="{Binding Name}" Foreground="Black" FontSize="18"

  20:                            FontWeight="ExtraLight" TextAlignment="Center" TextWrapping="Wrap"

  21:                            Grid.Row="1" VerticalAlignment="Bottom" />

  22:                 <TextBlock Text="{Binding Description}" Foreground="DarkGray"

  23:                            FontWeight="ExtraLight" FontSize="14" TextAlignment="Center"

  24:                            TextWrapping="Wrap" Grid.Row="2" VerticalAlignment="Top" />

  25:             </Grid>

  26:         </DataTemplate>

  27:     </GridView.ItemTemplate>

  28: </GridView>

La visualizzazione a lista invece è la seguente:

   1: <GridView ItemsSource="{Binding Items}" x:Name="ControlPanelList"

   2:           HorizontalContentAlignment="Stretch">

   3:     <GridView.ItemTemplate>

   4:         <DataTemplate>

   5:             <Grid Margin="2" HorizontalAlignment="Stretch" Padding="4" BorderThickness="1">

   6:                 <Grid.ColumnDefinitions>

   7:                     <ColumnDefinition Width="60" />

   8:                     <ColumnDefinition />

   9:                 </Grid.ColumnDefinitions>

  10:                 <Grid.RowDefinitions>

  11:                     <RowDefinition />

  12:                     <RowDefinition />

  13:                 </Grid.RowDefinitions>

  14:                 <TextBlock Grid.RowSpan="2" Text="{Binding Icon}" Foreground="Olive"

  15:                            FontFamily="Segoe UI Symbol" FontWeight="ExtraLight" FontSize="32"

  16:                            Grid.Row="0" VerticalAlignment="Bottom" Margin="8 0 0 0" />

  17:                 <TextBlock Text="{Binding Name}" Grid.Column="1" Foreground="Black"

  18:                            FontWeight="ExtraLight" FontSize="20" Grid.Row="0"

  19:                            VerticalAlignment="Bottom" Margin="8 0 0 0" />

  20:                 <TextBlock Text="{Binding Description}" Grid.Column="1" Foreground="DarkGray"

  21:                            FontWeight="ExtraLight" FontSize="16" TextWrapping="Wrap"

  22:                            Grid.Row="1" VerticalAlignment="Top" Margin="8 0 0 0" />

  23:             </Grid>

  24:         </DataTemplate>

  25:     </GridView.ItemTemplate>

  26:     <GridView.ItemsPanel>

  27:         <ItemsPanelTemplate>

  28:             <ItemsStackPanel Orientation="Vertical" />

  29:         </ItemsPanelTemplate>

  30:     </GridView.ItemsPanel>

  31:     <GridView.ItemContainerStyle>

  32:         <Style TargetType="GridViewItem">

  33:             <Setter Property="HorizontalContentAlignment" Value="Stretch" />

  34:         </Style>

  35:     </GridView.ItemContainerStyle>

  36: </GridView>

Notare che in ambedue i casi si tratta sempre di una GridView, solo che nel secondo caso (modalità lista) ho cambiato il pannello di default, impostandolo su:

   1: <GridView.ItemsPanel>

   2:     <ItemsPanelTemplate>

   3:         <ItemsStackPanel Orientation="Vertical" />

   4:     </ItemsPanelTemplate>

   5: </GridView.ItemsPanel>

In questo modo, pur trattandosi di una GridView, in realtà gli Items internamente vengono visualizzati con uno StackPanel, e quindi verticalmente. Perchè questo?

L’effetto di “OnMouseOver” di una GridView renderizza il bordo dell’elemento, come si vede nel video, e questo è proprio l’effetto che fa il pannello di controllo di Windows, ed è quello che volevo. La ListView, invece, renderizza il backgrund dell’elemento, e non è l’effetto che cercavo. Quindi: GridView in tutti e due i casi, solo che la modalità a lista utilizza uno StackPanel verticale. Più semplice di così si muore.

Ma arriviamo al sodo. Come ho utilizzato gli AdaptiveTrigger per switchare da una modalità all’altra? Con questo blocco di XAML:

   1: <VisualStateManager.VisualStateGroups>

   2:     <VisualStateGroup x:Name="VisualStateGroup">

   3:         <VisualState x:Name="VisualStateNarrow">

   4:             <VisualState.StateTriggers>

   5:                 <AdaptiveTrigger MinWindowWidth="0" />

   6:             </VisualState.StateTriggers>

   7:             <Storyboard>

   8:                 <DoubleAnimation Duration="0"

   9:                     Storyboard.TargetProperty="Opacity"

  10:                     Storyboard.TargetName="ControlPanelGrid"

  11:                     To="0" d:IsOptimized="True" />

  12:                 <ObjectAnimationUsingKeyFrames Duration="0"

  13:                     Storyboard.TargetProperty="(UIElement.IsHitTestVisible)"

  14:                     Storyboard.TargetName="ControlPanelGrid">

  15:                     <DiscreteObjectKeyFrame KeyTime="0" Value="False" />

  16:                 </ObjectAnimationUsingKeyFrames>

  17:             </Storyboard>

  18:         </VisualState>

  19:         <VisualState x:Name="VisualStateWide">

  20:             <VisualState.StateTriggers>

  21:                 <AdaptiveTrigger MinWindowWidth="600" />

  22:             </VisualState.StateTriggers>

  23:             <Storyboard>

  24:                 <DoubleAnimation Duration="0"

  25:                     Storyboard.TargetProperty="Opacity"

  26:                     Storyboard.TargetName="ControlPanelList"

  27:                     To="0" d:IsOptimized="True" />

  28:                 <ObjectAnimationUsingKeyFrames Duration="0"

  29:                     Storyboard.TargetProperty="(UIElement.IsHitTestVisible)"

  30:                     Storyboard.TargetName="ControlPanelList">

  31:                     <DiscreteObjectKeyFrame KeyTime="0" Value="False" />

  32:                 </ObjectAnimationUsingKeyFrames>

  33:             </Storyboard>

  34:         </VisualState>

  35:     </VisualStateGroup>

  36: </VisualStateManager.VisualStateGroups>

Nel Visual State Manager ho definito due viste: VisualStateNarrow e VisualStateWide.

La prima interviene con una Width minima di 0 pixel, ed attiva la visualizzazione degli elementi a lista, quindi modalità verticale. La seconda interviene con una Width minima di 600 pixel, ed attiva la visualizzazione degli elementi a griglia. Due note importanti:

  • per visualizzare o nascondere utilizzo la proprietà Opacity delle due GridView
  • di conseguenza ho dovuto impostare anche IsHitTestVisible a False, per fare in modo che un oggetto che nel visual tree compare “sopra” un altro, in realtà non partecipi all’intercettazione degli eventi di input. Uao, che frase complicata. Se non andassi ad impostare IsHitTestVisible a False, il controllo ControlPanelGrid non funzionerebbe come voluto, perchè davanti ci sarebbe il ControlPanelList (nonostante la sua Opacity a 0). Non è frutto del mio sacco, ho visto l’utilizzo di questa proprietà in Template 10, ed in effetti è una bella comodità

Direi che è tutto.

Il codice sorgente di questo progetto è su GitHub, e lo trovate qui.

Send to Kindle
.NET World

UWP e la Adaptive UI

Dunque, partiamo da qualche presupposto tecnologico.

Con Windows 8 e Windows 8.1 TUTTE le Windows Store App venivano disegnate per essere visualizzate “solo” in full-screen. Il “solo” l’ho messo tra virgolette perchè c’era un altra casistica: quando le app venivano snappate sul lato sinistro o destro dello schermo. E siccome stiamo parlando di app per Windows bisognava pensarle per tutte le risoluzioni possibili ed immaginabili. E bisognava pensarle sia in landscape che portrait. In ogni caso, l’utente non aveva la possibilità di ridimensionare l’app, perchè di fatto non c’era alcuna finestra con nessun bordo.

Con Windows 10 le cose sono un po’ diverse. Innanzitutto esiste la Modalità Tablet, che se attivata fa agire le nuove Universal Windows App (UWP) esattamente come sotto Windows 8/8.1. Partono in full-screen, possiamo snapparle, etc. etc. Quando la Modalità Tablet è spenta, le UWP vengono inserite in una finestra, al punto che l’utente visivamente non le distingue più dalle classiche applicazioni desktop. L’utente può ridurre ad icona l’app, può spostare la finestra e soprattutto può ridimensionarla.

E qui arriva il tema della Adaptive UI offerta dall’SDK di Windows 10. Non è scopo di questo post raccontare tutti gli aspetti della Adaptive UI, anche perchè io stesso ho ancora un po’ di confusione in testa. Lo scopo di per sè è piuttosto semplice:

Permettere ad una Universal Windows App di adattare il proprio contenuto in base a risoluzione & dimensione.

Ricordo, punto fondamentale, che un’app UWP gira ovunque sia presente Windows 10: desktop, portatile, mega-schermo, in futuro smartphone, XBOX, e via dicendo. Quindi, sostanzialmente, significa adattare il contenuto in base alla risoluzione/dimensione e fare in modo che il tutto sia sempre fruibile nel migliore dei modi. E quindi parliamo di schermi che vanno dal 10” di un ultrabook al 22” di un desktop, al 5” di uno smartphone (e la cosa divertente è che magari la risoluzione è sempre Full-HD).

Ecco una serie di articoli che trattano meglio questo argomento:

A Developer’s Guide to Windows 10: (07) Adaptive UI

Windows 10 Universal Apps – Adaptive Triggers

Template 10

Parlando di Template 10 mi sono guardato e studiato questa MainPage.xaml inserita nel progetto Sample. E’ un ottimo esempio di come la view possa reagire in base alla dimensione della finestra.

snip

Analizzando questa view, da cui sono partito, vi propongo una mia soluzione, che illustro in questo post. Esso utilizza ovviamente l’SDK di UWP, AdaptiveTrigger, Model-View-ViewModel con MvvmLight, e qualche altra nozione indispensabile.

Introduzione
Supponiamo di avere un’app UWP che mostra un elenco di automobili.

Ecco la nostra “bellissima” UI:

image

Qui abbiamo un MainViewModel che governa l’intera interfaccia. Questo viewmodel espone una proprietà Cars di tipo ObservableCollection<Car>. I dati sono stati inseriti manualmente nel costruttore. La UI è composta da un Hub con 3 HubSection al suo interno, uguali uno all’altro, sia nell’aspetto che nei dati visualizzati:

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
    DataContext="{StaticResource vm}">
    <Hub Grid.Row="1" x:Name="HorizontalView">
        <HubSection x:Name="CarsList1" Header="car list 1" Width="512"
                    HorizontalContentAlignment="Stretch"
                ContentTemplate="{StaticResource CarsListTemplateWide}" />
        <HubSection x:Name="CarsList2" Header="car list 2" Width="512"
                HorizontalContentAlignment="Stretch"
                ContentTemplate="{StaticResource CarsListTemplateWide}" />
        <HubSection x:Name="CarsList3" Header="car list 3" Width="512"
                HorizontalContentAlignment="Stretch"
                ContentTemplate="{StaticResource CarsListTemplateWide}" />
    </Hub>
</Grid>

Il contenuto di ciascun HubSection è prelevato dal DataTemplate CarsListTemplateWide.

In questo momento la nostra UI non è assolutamente adattiva. Se lanciamo l’app e ridimensioniamo la finestra, la UI non reagisce in alcun modo: al massimo ottenete ScrollBar verticali o orizzontali per permettere all’utente di scorrere e vedere tutto il contenuto.

Ciò che ci permettono di fare gli Adaptive Trigger di UWP è quello di intercettare i cambiamenti delle dimensioni e reagire in modo opportuno. Il tutto in modo dichiarativo usando lo XAML, senza scrivere alcuna linea di codice C#.

Concettualmente, quindi, possiamo andare a cambiare il ContentTemplate di ciascuno di quei 3 HubSection, switchando da CarsListTemplateWide a, per esempio, CarsListTemplateSmall. E supponiamo di adattare la UI dicendo: se la nostra finestra si stringe fino ad un certo limite, allora facciamo in modo che l’elenco delle automobili si adatti, rimuovendo alcune informazioni che decidiamo essere superflue.

Questa è la seconda visualizzazione che implementa questa logica. Stringendo la finestra, due colonne ad un certo punto scompaiono (quelle relative al modello e all’anno di immatricolazione).

image

Questo grazie al seguente blocco di XAML:

<VisualStateManager.VisualStateGroups>
	<VisualStateGroup x:Name="VisualStateGroup">
		<VisualState x:Name="VisualStateWide">
			<VisualState.StateTriggers>
				<AdaptiveTrigger MinWindowWidth="600" />
			</VisualState.StateTriggers>
			<VisualState.Setters>
				<Setter Target="CarsList1.ContentTemplate"
					Value="{StaticResource CarsListTemplateSmall}" />
				<Setter Target="CarsList2.ContentTemplate"
					Value="{StaticResource CarsListTemplateSmall}" />
				<Setter Target="CarsList3.ContentTemplate"
					Value="{StaticResource CarsListTemplateSmall}" />
				<Setter Target="CarsList1.Width" Value="300" />
				<Setter Target="CarsList2.Width" Value="300" />
				<Setter Target="CarsList3.Width" Value="300" />
			</VisualState.Setters>
		</VisualState>
		<VisualState x:Name="VisualStateVeryWide">
			<VisualState.StateTriggers>
				<AdaptiveTrigger MinWindowWidth="1024" />
			</VisualState.StateTriggers>
			<VisualState.Setters>
				<Setter Target="CarsList1.ContentTemplate"
					Value="{StaticResource CarsListTemplateWide}" />
				<Setter Target="CarsList2.ContentTemplate"
					Value="{StaticResource CarsListTemplateWide}" />
				<Setter Target="CarsList3.ContentTemplate"
					Value="{StaticResource CarsListTemplateWide}" />
				<Setter Target="CarsList1.Width" Value="512" />
				<Setter Target="CarsList2.Width" Value="512" />
				<Setter Target="CarsList3.Width" Value="512" />
			</VisualState.Setters>
		</VisualState>
	</VisualStateGroup>
</VisualStateManager.VisualStateGroups>

Vengono definiti 2 VisualState: uno è VisualStateWide, l’altro è VisualStateVeryWide. Il primo è valido dai 600 pixel in su, il secondo dai 1024 pixel in su. Giocando con i Setter del Visual State Manager vado a cambiare alcune proprietà degli oggetti sulla UI, principalmente due:

  • la larghezza degli HubSection (300 pixel oppure 512)
  • Il ContentTemplate degli HubSection (CarsListTemplateSmall oppure CarsListTemplateWide)

Il Setter agisce in modo immediato, e la UI si aggiorna come stabilito.

Conclusioni

Questa soluzione è molto carina da vedere in esecuzione. In forma statica, qui sul blog, rende molto poco l’idea. E soprattutto ha molto più senso in un’applicazione reale complessa, con molte informazioni da mostrare: in un esempio semplice come questo non risaltano bene le potenzialità. Pian piano, forse, l’Adaptive UI mi entra in testa!

Send to Kindle
.NET World

SignalR e notifiche di allarmi su diversi tipi di client, desktop e mobile

Introduzione
E’ da molto tempo che volevo studiare, almeno superficialmente, SignalR. Sebbene non segua molto le tecnologie Web, ogni tanto nella mia testa suona una specie di campanello di allarme (o di notifica, meglio), che mi dice: “Ok, quella cosa riguarda il Web, ma sento che potrebbe essere utile anche a me“. Anche perchè, parliamoci chiaro, io del Web odio solamente l’HTML ed i browser: la parte più stupida del client, insomma: passatemi questa definizione. SignalR è uno di quei topic che hanno fatto suonare quel campanello, senza mai trovare davvero il tempo e le risorse da dedicarci. Fino alla settimana scorsa, quando mio fratello Omar, improvvisamente, senza alcun input da parte mia, mi manda un bel sample proprio su SignalR. Uno di quei sample perfetti, di quelli che apri con Visual Studio, premi F5, compila e parte tutto, capisci alla perfezione, e poi passi ad analizzare il codice. Ecco, è andata proprio così. E quindi, grazie a quel sample ed all’intervento di mio fratello, è nato questo post.

Di cosa parliamo?
La settimana scorsa mi sono messo ad implementare un piccolo prototipo, che poi ho pubblicato sul mio account GitHub, che implementa la seguente struttura:

WP_20150902_10_26_09_Pro

In parole povere, abbiamo i seguenti componenti:

  • un device fisico, rappresentato a sinistra, cioè un dispositivo di qualche tipo (un sensore di temperatura? un frigorifero? un’auto? una caldaia? oppure…un razzo?). Questo dispositivo, tra le altre cose, è in grado di rilevare i suoi allarmi interni (temperatura alta, fuori traiettoria, perdita liquido refrigerante, motore fuori servizio), e di comunicarli a qualcuno
  • questo “qualcuno” è un dispatcher, rappresentato al centro, che nel mio caso è un server implementato con SignalR, server che ho pubblicato su Azure, quindi raggiungibile in modo globale. Qual è il suo compito? Ogni volta che riceve un allarme, avvisa tutti i client che risultano connessi, inviando loro un messaggio (leggesi: una classe DTO)
  • I client di qualsiasi tipo, rappresentati a destra, ricevono il messaggio ed avvisano l’utente nel modo più consono ed opportuno. Ciascun client all’avvio si collega al server SignalR implementato nel punto precedente di questo elenco

Trovate qui il progetto: https://github.com/VivendoByte/AlarmNotifier.

E’ importante che la cosa sia il più possible in real-time, ed è per questo che ho deciso di studiare e di passare attraverso SignalR.

Qualche dettaglio tecnico in più
Diamo un’occhiata al Solution Explorer che vi trovate nel momento in cui aprite la solution che ho pubblicato. Eccolo qui:

solution_explorer

  • C’è una solution folder denominata Client, al cui interno ho messo 4 tipi diversi di progetto: Console, Universal App per Windows 10, Windows Phone 8.1 e classica applicazione desktop in WPF. Questi progetti rappresentano i tipi diversi di client che possono ricevere la notifica di allarme
  • C’è una solution folder denominata Dispatcher, al cui interno ho messo 2 tipi diversi di progetto: Console e Web. Questi due progetti rappresentano il dispatcher degli allarmi. Uno è console e quindi apre un server SignalR sul vostro PC. L’altro è un progetto di tipo Web, ed è lo stesso che ho pubblicato su Azure
  • Infine, ci sono altri due progetti: uno è VivendoByte.AlarmNotifier.Messages, una class library di tipo Portable, che contiene solo una piccola classe DTO per poter inviare e ricevere il messaggio. L’altro progetto è VivendoByte.AlarmNotifier.RocketDevice, che rappresenta il nostro device fisico capace di generare allarmi: è un po’ una cosa brutta, effettivamente, ma l’ho implementato come applicazione WPF, così è possibile premere un bottone e simulare lo scatenarsi di un allarme

Parte Server – Il Dispatcher
Vediamo per prima cosa la parte server di SignalR. il punto focale è l’Hub, che io ho chiamato NotifierHub e che ho implementato come segue:

[sourcecode language='csharp'  padlinenumbers='true']
public class NotifierHub : Hub
{
	public void RaiseAlarm(AlarmMessage notification)
	{
		Clients.All.SendNotification(notification);
	}
	public override Task OnConnected()
	{
		return base.OnConnected();
	}

	public override Task OnDisconnected(bool stopCalled)
	{
		return base.OnDisconnected(stopCalled);
	}
}
[/sourcecode]

Il metodo RaiseAlarm che vedete implementato qui sopra viene invocato dal device (lo vediamo dopo), nel nostro caso il razzo. Ogni volta che il razzo segnala un allarme, si entra nel metodo RaiseAlarm; l’istanza di AlarmMessage che arriva come parametro contiene tutti i dettagli sull’allarme: data/ora, gravità, descrizione, eccetera. Quello che fa SignalR è rigirare l’allarme a tutti i client che risultano connessi in quel momento, con quella semplice linea di codice. Il dispatcher fa solo questo: riceve un allarme e lo rimanda ai client. Da notare due cose:

  • la proprietà All è di tipo dynamictooltip
  • il metodo SendNotification, che qui chiamiamo internamente al dispatcher, viene poi invocato anche sui client: lo vedremo più tardi

Il device fisico – Il Razzo

Adesso vediamo in breve il dispositivo fisico. Questo è il device capace di segnalare gli allarmi. Come dicevo prima, l’ho scritto in WPF, così ha un’interfaccia utente per poter premere un bottone e scatenare un allarme. Il code-behind è piuttosto semplice. Per prima cosa, ho implementato un metodo chiamato Connect:

[sourcecode language='csharp' ]
public MainWindow()
{
    InitializeComponent();
    this.Connect();
}

private async void Connect()
{
    Connection = new HubConnection(ServerURI);
    HubProxy = Connection.CreateHubProxy("NotifierHub");

    try
    {
        await Connection.Start();
    }
    catch (HttpRequestException)
    {
        return;
    }
}
[/sourcecode]

Questo metodo viene eseguito dal costruttore della finestra. In questo modo il nostro razzo si connette immediatamente al server SignalR, e ci mettiamo nella condizione di poter comunicare gli allarmi. Notate che il parametro del metodo CreateHubProxy, di tipo stringa, deve corrispondere al nome della classe Hub definita server-side. Quando nel razzo si scatena un allarme (ripeto: nell’esempio dobbiamo premere manualmente un bottone) accade quanto segue:

[sourcecode language='csharp' ]
private void SendAlarm_Click(object sender, RoutedEventArgs e)
{
    AlarmMessage a = new AlarmMessage();
    HubProxy.Invoke("RaiseAlarm", a);
}
[/sourcecode]

Molto semplice, direi. Costruisco un’istanza di AlarmMessage e la mando al server SignalR di cui abbiamo parlato prima. Il nome del metodo RaiseAlarm è espresso tramite una stringa, ed è quello che abbiamo visto implementato prima nel NotifierHub.

Ovviamente, in produzione un device comunicherebbe un allarme nel momento in cui si scatena realmente, e non c’è alcun utente a premere un bottone.

Parte Client – I diversi tipi di client

La parte client è l’ultima parte di tutto questo giro. Nel progetto su GitHub ne ho implementati quattro tipi diversi, qui parliamo solo della Console Application perchè è la più semplice da trattare. E’ importante dire che il codice per la connessione al server SignalR è lo stesso: diverso semmai è il comportamento che il cliente assume nel momento in cui riceve l’allarme, perchè ogni client ha un’interfaccia diversa e quindi comunica all’utente in modo diverso.

Ecco il codice:

[sourcecode language='csharp' ]
static void Main(string[] args)
{
    Connect();
}
static async void Connect()
{
    Connection = new HubConnection(ServerURI);
    HubProxy = Connection.CreateHubProxy("NotifierHub");

    HubProxy.On<AlarmMessage>("SendNotification", (notification) =>
        {
            System.Console.WriteLine(notification);
        }
    );

    try
    {
        Connection.Start();
    }
    catch (HttpRequestException)
    {
        return;
    }

    System.Console.WriteLine("Console Client started!!!");
    System.Console.ReadKey();
}
[/sourcecode]

Il metodo Connect crea un’istanza di HubProxy, specificando “NotifierHub” come parametro. Si sottoscrive alla ricezione del messaggio AlarmMessage. Il tutto viene gestito in questo caso tramite un anonymous method: quando viene ricevuto un allarme, la Console Application non fa nient’altro che scriverlo sulla console. Nel caso di applicazioni più complesse, come ad esempio un’applicazione WPF, potremmo aggiungere l’oggetto AlarmMessage ad una ListBox, oppure mostrare un balloon di notifica sullo schermo. Il primo parametro del metodo On è “SendNotification”, ed è quello che abbiamo specificato sul server SignalR; il secondo parametro è un Action<T>, dove T nel nostro caso è AlarmMessage.

Conclusioni

SignalR, come recita il sito ufficiale, è una libreria per sviluppatori ASP.NET in grado di implementare con poco sforzo una comunicazione real-time alle nostre applicazioni. E per citare… “What is “real-time web” functionality? It’s the ability to have your server-side code push content to the connected clients as it happens, in real-time”. Direi che è uno scenario perfetto per la comunicazione di allarmi, ma anche per applicazioni di chat, o di videogiochi, o per tutto quello che ci passa per la testa.

Per chiudere, fate clone dal mio repository su GitHub e provate a dargli un’occhiata.

Send to Kindle
Software

Windows 10 e Remote Desktop

Ho migrato entrambi i miei PC a Windows 10, ed ho avuto difficoltà ad effettuare connessioni con Remote Desktop. Il motivo è che di default la connessione di rete che Windows 10 imposta (cablata o WiFi che sia) è di tipo pubblico, e quindi connessioni RDP su questo tipo è negata.

Per risolvere è sufficiente seguire questi passi:

  1. cliccare con il destro sull’icona della connessione di rete e raggiungere il Centro connessioni di rete

    rete01

  2. Cliccare su Gruppo Home in basso a sinistra
  3. Cliccare il tipo di rete da Pubblico a Privato, evitando – se non serve – di connettersi ad un Gruppo Home come viene suggerito di fare

    rete02

  4. Non appena il tipo di rete cambia e diventa privata, Windows vi propone di andare alla ricerca sulla vostra LAN di tutti i dispositivi di rete, come stampanti e TV. Accettate se vi fa comodo

Fatto questo, le connessioni RDP dovrebbero funzionare regolarmente.

Il link originale da cui ho tratto questo post, e grazie al quale ho risolto il problema, è il seguente : http://www.tenforums.com/tutorials/3982-rdc-connect-remotely-your-windows-10-pc.html.

Send to Kindle
Software

Lancio Windows 10, io c’ero!

Lo scorso 29 Luglio ero nella sede Microsoft Italia di Peschiera Borromeo per partecipare insieme ad  un sacco di gente al lancio dell’ultimo sistema operativo di casa Microsoft. Una bellissima giornata. Un sacco di chiaccherate, un sacco di gente nuova, un grande casino, una fetta di torta, qualche gadget veramente figo. Felice di aver conosciuto, tra le altre persone, anche Evita Barra, sorella di Francesca, persone che seguo sui social da un po’ di tempo per motivi differenti (la prima – evidentemente – lavora in Microsoft; la seconda – invece – è scrittrice/giornalista/presentatrice di fama nazionale, di cui compro libri, mi appassiona il suo lavoro, ed ho redatto la sua pagina Wikipedia). Effetto strano quello di aver reincontrato in quel contesto alcuni dei ragazzi del progetto Skills4You a cui ho partecipato come docente.

WP_20150729_13_43_54_Pro__highres

Detto questo, la mia storia d’amore da Windows Insider con Windows 10 è cominciata maluccio, perchè le prime build proprio non mi convincevano.

WP_20150729_13_46_40_Pro__highres

Ma man mano che si avvicinava il 29 Luglio, vedevo le cose migliorare, fino ad arrivare alla versione attuale decisamente più funzionale ed adatta alla distribuzione in tutto il mondo.

Sono stato anche intervistato, insieme ad altri amici della community, per la mia esperienza da Windows Insider. Il video è su YouTube ed è visibile qui.

Felicissimo di essere stato uno dei protagonisti di questa magnifica giornata!!!!

Send to Kindle
.NET World

[UWP] Gestire dimensione e posizione delle finestre in un’app Windows 10

In questi giorni sto indagando pesantemente sulle modalità di funzionamento delle finestre del mondo di Universal Windows Platform, ed ho notato che ci sono ovviamente delle pesanti differenze con WPF. Mi sono schiarito un po’ le idee, ed ho deciso di scrivere questo post per riassumere un po’ le cose.

Windows Presentation Foundation

  1. Mondo a finestre, che possono girare sia come finestre sia a pieno schermo (com’è tradizione del caro buon vecchio mondo Windows, insomma)
  2. Possiamo impostare la dimensione e la posizione di ciascuna finestra, sia attraverso lo XAML che attraverso il codice
  3. Le applicazioni WPF per loro natura non ricordano la loro posizione e la loro dimensione: se vogliamo che un’app ricordi posizione & dimensione è tutto a carico del dev dell’applicazione stessa

Universal Windows Platform

Da notare che le linee di codice che vedrete qui sotto si riferiscono alla Release Candidate di Visual Studio 2015, ed alla Build 10240 di Windows 10. Le cose potrebbero cambiare quando il tutto sarà rilasciato in versione finale il 29 Luglio.

  1. Le app UWP possono girare in finestra, esattamente come una finestra WPF
  2. Le app UWP possono girare massimizzate, esattamente come una finestra WPF
  3. Possiamo impostare Windows 10 per girare in modalità Tablet, che “costringe” tutto il desktop a funzionare a pieno schermo. Le app in questo caso si comportano come in Windows 8.1
  4. Non posso mai impostare la posizione di una finestra (nè con XAML nè con codice): lieto di essere smentito
  5. Non posso mai impostare la dimensione di una finestra attraverso lo XAML, mentre da codice sì: lieto di essere smentito
  6. Le app UWP nativamente ricordano la loro ultima dimensione e posizione. Va da sè che quando l’utente riapre l’app XYZ la finestra si riaprirà nello stesso posto
  7. Posso evitare che un’app UWP ricordi la propria dimensione/posizione giocando con la proprietà ApplicationView.PreferredLaunchWindowingMode (che può valere Auto, FullScreen oppure PreferredLaunchViewSize)

Il tutto passa attraverso l’utilizzo della classe ApplicationView.

Chiedere ad un’app UWP di andare a pieno schermo

var view = ApplicationView.GetForCurrentView();
bool result = view.TryEnterFullScreenMode();

All’interno del codice di un’app UWP possiamo chiedere al sistema operativo di andare a pieno schermo. L’operazione può riuscire oppure no: ecco il motivo per cui restituisce bool. Evitate di impostare un breakpoint in corrispondenza della chiamata a TryEnterFullScreenMode(): l’esecuzione non va come dovrebbe.

Ovviamente possiamo uscire dalla modalità full-screen nel modo seguente:

view.ExitFullScreenMode();

In questo caso non abbiamo alcun valore di ritorno.

Possiamo controllare se la nostra app sta girando in full-screen con la proprietà:

view.IsFullScreenMode

Si tratta di una proprietà in sola lettura: l’unico modo che abbiamo per switchare da una modalità all’altra è quella di passare attraverso i metodi descritti precedentemente,

Chiedere ad un’app UWP se sta girando in tablet mode

La proprietà IsFullScreenMode – come abbiamo appena detto – ci dice se l’app sta girando in pieno schermo. Se vogliamo capire se Windows 10 sta girando in tablet mode possiamo interrogare la proprietà:

view.IsFullScreen

Anche in questo caso si tratta di una proprietà in sola lettura.

Chiedere ad un’app UWP di ridimensionarsi ad una certa dimensione

Dimenticatevi di impostare le proprietà ActualWidth e ActualHeight nello XAML.

Dimenticatevi di impostare le stesse proprietà da codice.

Se in un’app UWP volete modificare la dimensione di una finestra il codice è il seguente:

var view = ApplicationView.GetForCurrentView();
Size size = new Size(640, 480);
bool result = view.TryResizeView(size);

Anche in questo caso, possiamo capire se l’operazione è andata a buon fine oppure no controllando il valore di ritorno del metodo TryResizeView. Da notare che la dimensione minima di un’app UWP è 500 x 320: lieto di essere smentito. Se tentiamo di ridimensionare la finestra a, per esempio, 499 x 320, l’operazione fallisce, e TryResizeView ci restituisce false.

Ottenere le dimensioni correnti della finestra

In questo caso le cose sono più semplici, perchè è sufficiente leggere il valore delle proprietà ActualWidth e ActualHeight, esattamente come accadeva con le tecnologie precedenti.

Capire se la finestra è dockata a destra o a sinistra

Come in Windows 8.1, anche con Windows 10 posso agganciare una finestra al lato sinistro o destro del desktop. Per farlo, devo prima attivare la modalità tablet: l’app viene di conseguenza massimizzata sullo schermo, ed a questo punto la posso trascinare a destra o a sinistra, e posso anche ridimensionarla per la sua larghezza. Da codice posso scrivere quanto segue:

var view = ApplicationView.GetForCurrentView();
bool leftSide = view.AdjacentToLeftDisplayEdge;
bool rightSide = view.AdjacentToRightDisplayEdge;

Grazie alle proprietà AdjacentToLeftDisplayEdge e AdjacentToRightDisplayEdge (entrambe in sola lettura) posso eventualmente capire in quale situazione mi trovo. Ovviamente possono valere entrambe false in caso di finestra (normale o massimizzata), mentre non potrà mai accadere che entrambe valgano true.

Ottenere o modificare il titolo della nostra applicazione

var view = ApplicationView.GetForCurrentView();
view.Title = "mio titolo";

Le righe di codice qui sopra modificano il titolo dell’app in esecuzione. C’è una particolarità: la stringa che impostiamo viene sempre messa come prefisso rispetto al titolo stabilito nel file di manifest. Non so dire se è un comportamento voluto, oppure se è un qualche problema dovuto a Visual Studio 2015 RC o alla versione corrente di Windows 10.

Send to Kindle
My daily work

[OneNote.10] Spedire e condividere una singola pagina dei nostri appunti

Microsoft OneNote mette a disposizione della funzionalità per spedire o condividere una singola pagina dei nostri appunti. Questa operazione può avvenire secondo diversi formati o modalità, che verranno illustrate in questo decimo post dedicato a Microsoft OneNote.

Per prima cosa, è necessario aprire il blocco appunti e poi navigare nella sezione che contiene la pagina che ci interessa spedire ad un altro utente. Una volta selezionata la pagina (ad esempio…una ricetta, come nel mio esempio), apriamo il menù File e poi clicchiamo su Send.

image

Nell’esempio evidenziato qui sopra, abbiamo aperto il blocco appunti chiamato “Appunti Personali”, poi abbiamo aperto la sezione “Generale”, poi abbiamo selezionato la pagina “Ricetta Chisulin”. Poi, come abbiamo detto prima, clicchiamo su File in alto a sinistra e poi su Send.

image

Le possibilità di sending, ovvero di spedire la pagina corrente, sono le seguenti:

  • Email Page, ovvero spedire la pagina in una mail (il testo della pagina viene inserito direttamente nel corpo della mail stessa), come mostrato qui sotto:

    image

  • Send as Attachment, ovvero spedire la pagina come allegato di una mail, come mostrato qui sotto. La pagina viene inserita come allegato in due formati differenti: un file .one (formato standard di OneNote, ne avevamo parlato in questo post) ed un file .mht (formato tranquillamente apribile da Internet Explorer):

    image

  • Send as PDF, ovvero la pagina viene convertita in formato PDF, che poi viene inserito come allegato di una mail, come mostrato qui sotto:

    image

  • Send to Word, ovvero il testo della pagina viene inserito in un documento Word per una fase di editing più avanzato o complesso
  • Send to Blog, ovvero il testo della pagina viene inserito in un documento Word pronto per essere bloggato. Maggiori dettagli qui di seguito

Le prime quattro opzioni sono piuttosto intuitive. In qualche post precedente abbiamo descritto l’integrazione che OneNote offre nei confronti di Microsoft Outlook. Oggi stiamo invece parlando dell’integrazione tra OneNote e Microsoft Word. Nel caso più semplice (Send to Word) il testo della pagina viene iniettato così com’è in un nuovo documento Word. Nell’ultimo caso (Send to Blog) si sfrutta la possibilità che Word ha di connettersi a blog engine come WordPress.

Bloggare da Microsoft Word
Nel caso non abbiate mai connesso Microsoft Word al vostro blog, la prima cosa che vi apparirà dopo aver cliccato su “Send to Blog” da OneNote è la seguente:

image

Se clicchiamo su Register Now, ci verrà data la possibilità di connettere Microsoft Word al nostro blog, ovviamente fornendo dati come l’indirizzo web, il nome-utente e la password per la connessione.

image

image

Dipendentemente dal vostro blog, le informazioni potrebbero ovviamente variare.

Se clicchiamo su Register Later, potremo effettuare la connessione in un secondo momento. In ogni caso, quello che OneNote fa è iniettare in Word il contenuto della nostra pagina. Qui possiamo utilizzare tutti gli strumenti di editing del testo che Word mette a disposizione. Al termine dell’editing, clicchiamo sul pulsante Publish (in alto a sinistra nello screenshot qui sotto) per pubblicare il post sul blog configurato.

image

Per oggi è tutto! Alla prossima puntata!

Send to Kindle
My daily work

[OneNote.9] Integrazione con i task di Microsoft Outlook

Riprendiamo la serie dedicata a Microsoft OneNote parlando della sua capacità di integrazione con un altro applicativo incluso in Office, ovvero Microsoft Outlook.

In questo mio precedente post era stata descritta la possibilità di taggare una certa nota con uno dei tag disponibili. Questa cosa è possibile utilizzando la ribbon “Home”, all’interno della sezione Tags.

image

Nella stessa ribbon c’è anche una funzione denominata Outlook Tasks. Questa funzione permette di associare una nota con un task di Outlook. Un “task” è un certo compito (un to-do, insomma) da portare a termine, con un titolo ed una descrizione, con una data di inizio e di fine, e con tutta una serie di altre proprietà per poterlo

Se si clicca sull’icona vengono mostrate le opzioni:

image

La descrizione è piuttosto chiara. E’ possibile cliccare su:

  • Today, ovvero Oggi. In Microsoft Outlook viene inserito un task da completare entro oggi
  • Tomorrow, ovvero Domani. In Microsoft Outlook viene inserito un task da completare entro domani
  • This Week, ovvero Questa Settimana. In Microsoft Outlook viene inserito un task da completare il venerdì della settimana corrente
  • Next Week, ovvero Prossima Settimana. In Microsoft Outlook viene inserito un task che comincia il lunedì della settimana prossima, e che deve essere completato il venerdì successivo
  • No Date, ovvero Nessuna Data. In Microsoft Outlook viene inserito un task senza alcuna data di scadenza
  • Custom, ovvero Personalizzato. Se viene selezionata questa opzione, viene aperta la finestra di Microsoft Outlook che permette di compilare il task. Da questa finestra, è possibile modificare l’oggetto del task, la data di inizio e di fine, la priorità, la percentuale di completamento, impostare un promemoria. Questa è l’opzione migliore per utilizzare Outlook al pieno delle sue possibilità.

image

  • Delete Outlook Task, ovvero Cancellare il Task in Outlook. Questa opzione naturalmente si attiva nel momento in cui la nota è già associata ad un task precedentemente impostato. Questa opzione va utilizzata nel momento in cui vogliamo rimuovere il task
  • Open Task di Outlook, ovvero Aprire il Task in Outlook. Funzione molto semplice, che apre la finestra di Outlook per editare il task

Nel momento in cui una nota di Microsoft OneNote viene associata ad un task di Microsoft Outlook, essa viene evidenziata con una banderuola.

image

Il colore della banderuola cambia a seconda dell’urgenza del task stesso.

Non vedo i task in Microsoft Outlook: come posso fare?
Tutto questo meccanismo di integrazione è utile solo se poi effettivamente in Outlook possiamo vedere l’elenco dei task correntemente attivi. Nel mio caso, la visualizzazione dei task era disattivata. Per attivarla, ho dovuto:

  1. Spostarmi ed attivare la ribbon “View”
  2. Cliccare su To-Do Bar (rettangolo rosso qui sotto)
  3. Cliccare su Tasks (rettangolo verde qui sotto)

image

Fatto questo, Outlook visualizzerà un nuovo pannello, sulla destra, con l’elenco dei task attivi. Questa visualizzazione verrà mantenuta aggiornata con le note gestite attraverso OneNote (con alcune limitazioni, visto che non esiste un vero e proprio meccanismo di sincronizzazione).

image

All’interno di Outlook è possibile ordinare i task secondo diversi criteri e modificarli.

Cercare i task all’interno di Microsoft OneNote
Nella puntata n°4 abbiamo accennato al fatto che all’interno di OneNote è possibile cercare le note che hanno qualche tag associato. Anche le note che si sono evolute come task in Outlook cadono in questa categoria. Di conseguenza, è possibile cliccare sulla funzione Find Tags per ottenere velocemente l’elenco di tutti i task, senza per forza aprire Outlook.

image

Marcare un task come completato
Da OneNote possiamo marcare un task come completato. E’ sufficiente andare nel Tags Summary e poi cliccare sulla banderuola presente accanto al titolo del task stesso. La banderuola diventa un segno di spunta verde. Questa modifica ha immediato effetto anche in Microsoft Outlook, ovviamente, e difatti il task viene rimosso dall’elenco perchè completato.

image

Conclusioni
Microsoft OneNote può creare ed aggiornare task che vengono automaticamente inseriti in Microsoft Outlook. Ne abbiamo parlato in questo post, e la cosa è molto utile ed interessante, perchè la collaborazione e l’integrazione rendono le cose più semplici ed immediate.

Appuntamento alla prossima puntata!

Send to Kindle
My daily work

Windows 10 arriva il 29 Luglio, e noi dev dobbiamo essere pronti

Il dado è tratto. Microsoft ha ufficialmente comunicato che il prossimo 29 Luglio 2015 rilascerà la prossima versione del suo sistema operativo, ovvero Windows 10.

Per noi developer in particolare la cosa è particolarmente succulenta, perchè grazie alle nuove tecnologie UWP (Universal Windows Platform) e UWA (Universal Windows Application), saremo finalmente in grado di creare e sviluppare applicazioni in grado di girare indistintamente su smartphone, PC, ultrabook, XBOX, e così via (tenendo ovviamente in considerazione le opportune differenze in termini di hardware e di usability).

A questo indirizzo trovate un’interessante serie di lezioni (in inglese, senza sottotitoli) che vi illustrano i vari aspetti dello sviluppo di applicazioni per Windows 10. Le lezioni sono inserite nel contesto della Microsoft Virtual Academy, e trattano svariati argomenti: la nuova architettura di .NET e della Windows Platform, i nuovi controlli disponibili in XAML, come adattare le view ed il codice in base all’hardware, gestione di file & database, e molto altro ancora.

Direi che è un’occasione da non perdere. Windows 10 sta arrivando, non vorrete farvi trovare impreparati, no?

Send to Kindle
My personal life

Evoluzioni. Grazie a tutti, grazie a te!

Da qualche mese ormai la mia vita lavorativa è cambiata. Se ripenso al passato, a tutte le esperienze che mi hanno condotto fino a qui, mi rendo conto che ne ho passate di tutti i colori, nel bene e nel male. Ho lavorato all’inizio (circa 18-20 anni, durante la seconda metà degli anni ‘90) come assemblatore hardware pagato solo nei mesi estivi, ho fatto persino qualche mese lavorando con Autocad per MS-DOS, poi ho cominciato a muovere i primi passi come programmatore Visual Basic 6.0. Prima come dipendente qui a Sant’Angelo Lodigiano, per qualche anno, poi per quasi una decina d’anni come lavoratore in proprio (scrivendo e vendendo software miei – non potete immaginare l’orgoglio che si prova), poi come il classico consulente in partita iva (una volta entrato nel “girone infernale” delle società di consulenza che poi ti redirigono dal cliente finale). Ho conosciuto tantissime persone nel mondo delle community di noi developer (newsgroup, blog ed ovviamente anche community reali), e sono andato persino al funerale di un vecchio caro amico dev conosciuto su it.comp.lang.visual-basic (Ciao Lupo, non ti dimenticherò mai). Ho fatto il pendolare Lodi-Milano per molti anni, insomma, al punto che nel 2009 avevo (quasi) deciso di mollare tutto. Troppo lo stress, troppa la stanchezza di prendere la macchina tutti i giorni, troppo lo sbattimento di svegliarsi alle 6:30 del mattino per cominciare a lavorare davvero alle 9:00. Sono passato da vicende che oggi definisco tranquillamente di mobbing, sono rimasto bloccato nel traffico delle tangenziali per troppo a lungo, in mezzo alla nebbia, alla neve, con gli occhi incollati sugli stop rossi dell’auto che precede. Voi direte: come sei viziato! Facevi un lavoro che ti piace e ti lamentavi pure!!!! C’è gente che un lavoro non ce l’ha, tu sei fortunato!!! Io penso che un lavoro ti può pure piacere alla grande, ma se questo lavoro alla fine ti stanca e ti toglie vita privata, al punto che il sabato non hai più voglia di uscire, non hai più voglia di conoscere persone, nè di incontrare ragazze, c’è qualcosa che non va. “Un lavoro di qualità non può venire prima di una vita di qualità” – dice qualcuno – ed io quella vita di qualità non ce l’avevo proprio.

Ognuno di noi ha una propria soglia di sopportazione delle cose.
Io quella soglia l’ho raggiunta nell’estate del 2009.

Quell’anno, al rientro dalle ferie estive, ho cominciato a lavorare in Brain-Sys, piccola ma grande azienda che mi ha fatto davvero rinascere sotto tanti punti di vista. Grazie al lavoro da casa, ad una sola trasferta al mese, grazie alla partecipazione agli eventi di community, piano piano ho ritrovato me stesso. Il caro buon vecchio Liborio Igor.

Ma torniamo all’inizio di questo post. Da qualche mese ormai la mia vita lavorativa è cambiata. Ho superato dei limiti miei personali e caratteriali, limiti che credevo impossibili da raggiungere. Ho preso il toro per le corna ed ho affrontato le mie timidezze. Sono riuscito a parlare in pubblico, davanti a diverse decine di di persone, tra l’altro nella stessa saletta in cui il mio amico Lorenzo Barbieri esattamente una settimana prima aveva giust’appunto tenuto una sessione sul “parlare in pubblico” (non perdete la sua serie di post pubblicata su LinkedIn). Strano scherzo del destino, no?!? Ho cominciato a tenere corsi, davanti ad una, due, quattro, fino a dieci e più persone. Ho tenuto a Gennaio/Febbraio 2015 un corso serale sullo sviluppo di app per Windows Phone. I partecipanti dei corsi che tengo per OverNet a volte sono lì fisicamente, altre volte collegate in videoconferenza, altre volte ancora un mix di tutto questo. E’ molto istruttivo, perchè ogni volta che si incontra qualcuno, si impara qualcosa. E spiegando agli altri ci si rende che certe cose le hai capite davvero. E quando sistematicamente ti lasciano feedback positivi, rimarcando il fatto che trasmetti passione ed energia positiva, sono davvero felice e consapevole di aver fatto bene il mio lavoro. Sono davvero contento di me stesso, e non c’è nulla di più gratificante che rendersi conto di essere andati un pochino oltre i propri limiti. Mai e poi mai avrei creduto che un giorno avrei fatto tutto questo, dico davvero.

Devo ringraziare un po’ di persone, e sinceramente faccio fatica a stabilirne l’ordine. Beh, direi di partire da tutti noi di Brain-Sys, dai capi a tutti gli altri componenti del nostro gruppo. Senza il loro lavoro, non ci potrebbe essere di conseguenza il mio. Grazie a loro sono migliorato, sia da un punto di vista personale che professionale. Grazie alla mia famiglia, che mi sopporta, e che a volte fa fatica a capire il mio lavoro.

Ma c’è una persona speciale che devo ringraziare, una persona che da quando è entrata nella mia vita non ha fatto altro che portare luce e tanti tanti sorrisi. Sapete, solitamente si ringrazia la moglie o la propria ragazza dalle pagine di un libro cartaceo, o comunque in corrispondenza di una data particolare. Io no. Io voglio farlo oggi, in un giorno qualunque (e quindi allo stesso tempo speciale come qualsiasi altro), dalle pagine digitali del mio blog. Grazie Fede, di esserci e di esistere, di dare un senso alle mie giornate e di spingermi inconsapevolmente a diventare una persona sempre migliore. Se ho superato quei limiti, lo devo anche a te. Perchè tutte le volte che sono andato a dormire in ansia, pensando a quello che mi aspettava il giorno dopo – un ennesimo limite da superare, un piccolo gradino – ho pensato a te ed al tuo sorriso. Grazie incredibilmente per apprezzare il mio lavoro (che non è così banale) e grazie per esultare con me dei miei piccoli successi quotidiani. Grazie per i consigli che mi dai, e grazie anche perchè comprendi quando ho bisogno del mio tempo per studiare, per preparare slide o test di verifica. Dietro un uomo c’è sempre una grande donna, e quella donna per me sei Tu.

Oggi sono felice. Un po’ con la schiena rotta, ma felice.

Send to Kindle