MSDN Tips & Tricks

I consigli degli esperti italiani per sfruttare al meglio gli strumenti di sviluppo e semplificare l’attività quotidiana.

In questa pagina

Personalizzare il layout delle ListBox in Windows Presentation Foundation Personalizzare il layout delle ListBox in Windows Presentation Foundation
Raggruppare e mostrare i dati con Windows Presentation Foundation Raggruppare e mostrare i dati con Windows Presentation Foundation
Gestione delle SecurityException avviando l’applicazione da uno share di rete Gestione delle SecurityException avviando l’applicazione da uno share di rete
Rendere in forma canonica i nomi dei file Rendere in forma canonica i nomi dei file

Personalizzare il layout delle ListBox in Windows Presentation Foundation

Di Cristian Civera - Microsoft MVP

Windows Presentation Foundation contiene un ricco set di controlli e una complessa architettura che ha però il pregio di rendere l'interfaccia grafica estremamente versatile. Rispetto al passato, le classi di WPF sono infatti dei contenitori logici di informazione, a prescindere da quello che può essere il loro aspetto. Spetta poi alle risorse e agli stili definiti, a livello di tema del sistema operativo oppure personalizzati per l'applicazione, fare uso di pannelli, immagini o disegni vettoriali per formare l'interfaccia che mostra e interagisce con l'utente.

Il controllo ListBox, per esempio, eredita da ItemsControl e lo estende fornendo informazioni di multi selezione degli elementi, ma è lo stile indicato per il tipo ListBox e per gli elementi ListBoxItem a fornire un look and feel simile a quello che solitamente si è abituati a utilizzare con le Win32. Normalmente, il pannello che contiene tutti gli elementi è un VirtualizingStackPanel, un'evoluzione di StackPanel che allinea i figli dall'alto al basso (o da sinistra verso destra), evitando, al fine di ottimizzare le prestazioni, di creare e allineare gli elementi che non sono visibili.

Questo comportamento si può cambiare mediante la proprietà ItemsPanel definendo un template di pannello da utilizzare. Nell'esempio seguente si fa uso del WrapPanel per allineare da sinistra verso destra e passando alla linea successiva, qualora si superi la dimensione consentita dal pannello:

<ListBox Width="250" Height="142" ItemsSource="{Binding Source={StaticResource data}}">
  <!-- Uso un Wrap invece del normale StackPanel -->
  <ListBox.ItemsPanel>
    <ItemsPanelTemplate>
      <WrapPanel Width="242" />
    </ItemsPanelTemplate>
  </ListBox.ItemsPanel>
</ListBox>

Generato il pannello contenitore, per ogni elemento caricato tramite la proprietà ItemsSource viene generato un container di tipo ListBoxItem. Sulla base di quest'ultimo è definito uno stile che mediante Border permette di evidenziare l'oggetto selezionato e di cambiarne il colore. E' possibile modificare anche questo comportamento mediante la proprietà ItemContainerStyle ridefinendo l'intero template del contenitore di ogni elemento. L'esempio seguente ingrandisce l'elemento quando è selezionato mediante trigger, basato sulla proprietà IsSelected, e una trasformazione:

<ListBox ...>
  <!-- ridefinisco il container per fornire un effetto di selezione diverso-->
  <ListBox.ItemContainerStyle>
    <Style TargetType="{x:Type ListBoxItem}">
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="{x:Type ListBoxItem}">
            <ControlTemplate.Triggers>
              <Trigger Property="IsSelected" Value="true">
                <Setter Property="RenderTransform">
                  <Setter.Value>
                    <TransformGroup>
                      <TranslateTransform X="-2" Y="-2" />
                      <ScaleTransform ScaleX="1.4" ScaleY="1.4" />
                    </TransformGroup>
                  </Setter.Value>
                </Setter>
                <Setter Property="Canvas.ZIndex" Value="1" />
              </Trigger>
            </ControlTemplate.Triggers>
            
            <ContentPresenter />
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>
  </ListBox.ItemContainerStyle>

Durante la fase di generazione dei container viene poi associato l'elemento che si sta caricando, e su di esso applicato un DataTemplate qualora sia stato associato tramite la proprietà ItemTemplate o tramite stili.
L'esempio seguente sfrutta questa caratteristica per disegnare un rettangolo e colorarlo tramite l'elemento corrente (la risorsa statica "data" è una lista di nomi di colori):

<!-- Template per ogni colore -->
<ListBox.ItemTemplate>
  <DataTemplate>
    <Rectangle Width="15" Height="15" Stroke="Gray" StrokeThickness="1" Fill="{Binding XPath=.}">
      <Rectangle.ToolTip>
        <TextBlock Text="{Binding XPath=.}" />
      </Rectangle.ToolTip>
    </Rectangle>
  </DataTemplate>
</ListBox.ItemTemplate>

Ecco un'immagine della ListBox completamente ridisegnata e funzionante come tavolozza di colori.

*

Si ricorda inoltre che le proprietà viste in questo script sono appartenenti a ItemsControl e quindi applicabili ai controlli ComboBox, ListBox, ListView, TabControl, TreeView, ToolBar e MenuItem.

Demo su http://lab.aspitalia.com/msdn/

 

Raggruppare e mostrare i dati con Windows Presentation Foundation

Di Cristian Civera - Microsoft MVP

Windows Presentation Foundation contiene un insieme di classi volte a gestire i dati in modo particolarmente avanzato e completo. L'architettura per caricare, mostrare e modificare i dati è frutto dell'esperienza maturata con le WinForms di Framework .NET 1.x e non è più limitata a singole unità di dati isolati, ma connesse tra loro così da consentire un sistema di caricamento dati automatico e sincronizzato.

Ogni ItemsControl (ComboBox, ListBox, TabControl, TreeView, ToolBar e MenuItem) dispone di una proprietà ItemsSource per poter caricare qualsiasi tipo di dato. Sulla base di esso viene sempre creata un'istanza di CollectionView che rappresenta appunto una vista e permette di filtrare, ordinare e raggruppare tutte le entità che costituiscono i dati.

Per consentire di dichiarare e configurare le viste direttamente da XAML, il tipo CollectionViewSource fornisce le proprietà Source, SortDescriptions e GroupDescriptions. Nello script successivo si pone di avere una sorgente XML rappresentante una lista di e-mail e su di esse viene creata una vista che raggruppa i dati per data (attributo date):

<Grid.Resources>
  <XmlDataProvider x:Key="data" XPath="/items/item">
    <x:XData>
      <items >
        <item date="16/03/2007">Newsletter ASPItalia.com</item>
        <item date="16/03/2007">Richiesta appuntamento</item>
        <item date="15/03/2007">Contratto lavoro</item>
        <item date="15/03/2007">[Spam] Diventa più bello</item>
        <item date="15/03/2007">MSDN Flash</item>
      </items>
    </x:XData>
  </XmlDataProvider>

  <CollectionViewSource x:Key="viewData" Source="{StaticResource data}">
    <CollectionViewSource.GroupDescriptions>
      <PropertyGroupDescription PropertyName="@date"/>
    </CollectionViewSource.GroupDescriptions>
  </CollectionViewSource>

</Grid.Resources>

Tramite le viste, se filtrare o ordinare i dati si traduce solo nel cambiare l'ordine o nel rimuovere le entità, il raggruppamento richiede al controllo ItemsControl di supportare questa funzionalità visivamente. La proprietà GroupStyle, a questo scopo, permette di definire il pannello contenitore di ogni gruppo di elementi, lo style del contenitore stesso e il template per le intestazioni.

L'esempio seguente personalizza l'intestazione di ogni gruppo indicando la data e quante e-mail contiene il gruppo.

<ListBox DisplayMemberPath="." ItemsSource="{Binding Source={StaticResource viewData}}">
  <ItemsControl.GroupStyle>
    <GroupStyle>
      <GroupStyle.HeaderTemplate>
        <DataTemplate>
          <StackPanel>
            <Line Stroke="Blue" StrokeThickness="1" X1="1" X2="300" />
            <TextBlock FontWeight="Bold">
              <Run>Date: </Run>
              <TextBlock Text="{Binding Path=Name}" />
              <Run FontWeight="Normal"> - </Run>
              <TextBlock FontWeight="Normal" Text="{Binding Path=ItemCount}"/>
            </TextBlock>
          </StackPanel>
        </DataTemplate>
      </GroupStyle.HeaderTemplate>
    </GroupStyle>
  </ItemsControl.GroupStyle>
</ListBox>

All'interno del template di intestazione, il contesto dei dati disponibile è un'istanza del tipo CollectionViewGroup che restituisce informazioni quali Name (il valore del gruppo), ItemCount (il numero degli elementi raggruppati) e IsBottomLevel (booleano che indica se è l'ultimo gruppo).

Il risultato visivo è il seguente:

*

 

Gestione delle SecurityException avviando l’applicazione da uno share di rete

Di Raffaele Rialdi - Microsoft MVP

Le applicazioni realizzate con il Framework.Net sono soggette al controllo della Code Access Security. Prendiamo un banale esempio in cui una applicazione Windows debba scrivere su disco un file. La FileIOPermission non è assegnata alla zona Intranet pertanto il lancio di questa applicazione dallo share di rete provocherebbe una SecurityException con il conseguente crash dell’applicazione.

Il crash avviene a seguito del controllo dell CAS la quale entra in gioco subito prima di eseguire il metodo che accede al File. Se quindi il file viene scritto nella Form Load avremmo il crash immediato, se invece è scritto dentro il gestore del click di un bottone, avverrà solo al suo click.

Purtroppo molti progammatori ritengono che la CAS sia un argomento di cui non curarsi se non in casi limite, e invece non è così. L’esempio appena fatto dimostra che è necessario conoscerla e gestire queste situazioni.

Per evitare la classica brutta figura con il cliente, la cosa migliore è fare uno specifico controllo al lancio dell’applicazione per verificare se l’applicazione abbia o meno i permessi necessari e conseguentemente decidere se proseguire o terminare avvisando l’utente. Diciamo di voler controllare di avere FullTrust, cioè nessuna limitazione da parte della CAS, quella che si ha normalmente quando l’applicazione è lanciata dal disco locale.

In Visual Basic le Windows Form vengono lanciate direttamente mentre in C# esiste una classe statica contenuta tipicamente dentro Program.cs che lancia la form. Per poter eseguire il controllo il programmatore VB 2005 dovrà creare ApplicationEvents.vb seguendo questi passi:

  • Dal solution explorer, doppio click su My Project

  • Scegliere il tab Application

  • Cliccare il bottone View Application Events

  • Posizionare il cursore dopo la riga di codice “Partial Friend Class ….”

  • Nella combo box sopra l’editor scegliere la voce MyApplication Events

  • Nella combo box a fianco scegliere l’evento di Startup

Il controllo è molto semplice e fa scaturire una SecurityException se fallisce:

C#

PermissionSet FullTrust = new PermissionSet(PermissionState.Unrestricted);
FullTrust.Demand();

VB

Dim FullTrust As New PermissionSet(PermissionState.Unrestricted)
FullTrust.Demand()

In sostanza mettendo con un try/catch queste due righe di codice, si può sapere subito se il codice ha diritto di FullTrust o meno.

Le cose si complicano leggermente qualora all’avvio dell’applicazione dovessimo anche eseguire del codice che richiede un livello di trust insufficiente come ad esempio la sottoscrizione agli eventi per gestire le eccezioni globali.

Siano questi eventi My.Application.UnhandledException, Application.ThreadException o AppDomain.UnhandledException, tutti questi richiedono privilegi non disponibili ad esempio in zona Intranet, perciò la sottoscrizione all’evento deve avvenire in un metodo differente.

Se infatti il metodo contenesse sia il controllo sopra citato che un’operazione passibile di SecurityException come la sottoscrizione all’evento, il controllo sarebbe ovviamente inutile. La soluzione è semplice e consiste nel relegare il codice che potenzialmente può scatenare la SecurityException a un livello di stack differente rispetto al controllo per verificare di avere FullTrust.

Nel codice VB la soluzione è leggermente differente perché non possiamo modificare direttamente il codice che fa la sottoscrizione all’evento. Si risolve però evitando di sottoscrivere l’evento di UnhandledException tramite wizard (combo box) ma la si esegue da codice nell’apposito metodo TrapEvent come mostrato nell’esempio.

Il risultato finale è il seguente:

C#

static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
if(!CheckSecurity())
return;

RunApp();
}

private static bool CheckSecurity()
{
try
{
PermissionSet FullTrust = new PermissionSet(PermissionState.Unrestricted);
FullTrust.Demand();
}
catch(SecurityException err)
{
StringBuilder sb = new StringBuilder();
sb.AppendFormat("L'applicazione è stata lanciata in zona {0}\r\n", err.Zone.ToString());
sb.AppendFormat("ma non ha il privilegio di FullTrust\r\n");

MessageBox.Show(sb.ToString(), "Privilegi insufficienti per l'applicazione");
return false;
}
return true;
}

private static void RunApp()
{
Application.ThreadException 
+= new System.Threading.ThreadExceptionEventHandler(Application_ThreadException);
AppDomain.CurrentDomain.UnhandledException 
+= new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);

Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}

static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
MessageBox.Show((e.ExceptionObject as Exception).Message, "Errore generale nell'applicazione");
}

static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
{
MessageBox.Show(e.Exception.Message, "Errore generale nell'applicazione");
}
}

VB

Partial Friend Class MyApplication

        Private Sub MyApplication_Startup(ByVal sender As Object,
 ByVal e As Microsoft.VisualBasic.ApplicationServices.StartupEventArgs) 
Handles Me.Startup

            Try
                Dim FullTrust As New PermissionSet(PermissionState.Unrestricted)
                FullTrust.Demand()

                TrapException()
            Catch err As SecurityException
                Dim sb As New StringBuilder()
                sb.AppendFormat("L'applicazione è stata lanciata in zona {0}" + vbCrLf, err.Zone.ToString())
                sb.AppendFormat("ma non ha il privilegio di FullTrust" + vbCrLf)

                MessageBox.Show(sb.ToString(), "Privilegi insufficienti per l'applicazione")
                e.Cancel = True
            End Try

        End Sub

        Sub TrapException()
            AddHandler My.Application.UnhandledException, AddressOf AppException
        End Sub


        Private Sub AppException(ByVal sender As Object, 
ByVal e As Microsoft.VisualBasic.ApplicationServices.UnhandledExceptionEventArgs)
            MessageBox.Show(e.Exception.Message, "Errore generale nell'applicazione")
        End Sub

End Class

 

Rendere in forma canonica i nomi dei file

Di Raffaele Rialdi - Microsoft MVP

La rappresentazione di un nome di file non è univoca e questo può essere un grosso problema quando il nome del file viene indicato dall’utente perché può sfuggire alla nostra validazione.

Per esempio potremmo volere che l’estensione del file non possa essere .mdb ma l’utente potrebbe ingannare la nostra validazione specificando nomefile.mdb. (con il punto finale). Per il sistema operativo non cambia nulla, ma la nostra validazione potrebbe fallire.

Ci sono molti esempi di equivalenze nei nomi dei file. Per esempio NomeFileLungo.txt è equivalente a NomeFileLungo.txt. e NomeFi~1.txt oppure a .\.\.\NomeFileLungo.txt o ancora a NomeFileLungo.txt::$data.

La sintassi “::$data” è una caratteristica specifica di NTFS che permette di indicare il nome di uno stream secondario di dati appartenente allo stesso file secondo la sintassi “nomefile.ext:nomestream:$tipostream”. In sostanza un qualsiasi file può contenere dei “sottofile” il cui contenuto è identificato grazie a quella sintassi particolare. Gli stream secondari sono invisibili da Explorer che mostra solo quello primario che è privo di nome ed il cui tipo è $data; da qui viene l’equivalenza tra NomeFileLungo.txt e NomeFileLungo.txt::$data.

Il processo di rendere uniformi i nomi dei file viene chiamato canonicalizzazione perché rende canonica la forma in cui è rappresentato il percorso e il nome del file.

Il metodo del Framework.Net Path.GetFullPath risolve buona parte dei problemi, ma non tutti. Non risolve ad esempio i nomi corti in lunghi in quanto la risoluzione è possibile solo se il file esiste su disco; è infatti importante considerare anche i casi in cui il file debba essere creato su disco e che quindi non esista ancora. Inoltre è prudente conseiderare non validi gli stream secondari piuttosto che rischiare un accesso non desiderato.

Considerando tutte le casistiche appena citate ecco una possibile soluzione al problema. Questo metodo recupera la path completa del file specificato. In caso di solo nome di file, la cartella è naturalmente relativa alla nostra applicazione:

C#

string CheckFile(string str)
{
try
{
str = Path.GetFullPath(str);
if(str.Contains("~"))
{
string[] Result = Directory.GetFiles(Path.GetDirectoryName(str),
 Path.GetFileName(str));
if(Result.Length == 0)
return str;
return Path.GetFileNameWithoutExtension(Result[0]);
}
return str;
}
catch(Exception)
{
return string.Empty;
}
}

VB

Public Function CheckFile(ByVal str As String) As String
        Try
            str = Path.GetFullPath(str)
            If (str.Contains("~")) Then
                Dim Result As String() = Directory.GetFiles(Path.GetDirectoryName(str), 
Path.GetFileName(str))
                If (Result.Length = 0) Then Return str
                Return Path.GetFileNameWithoutExtension(Result(0))
            End If
            Return str
        Catch ex As Exception
            Return String.Empty
        End Try
    End Function

All’interno della if che controlla se è presente la tilde “~” è possibile usare via PInvoke la API GetLongPathName, probabilmente più efficiente ma che costringe l’applicazione ad avere i permessi di usare codice unmanaged, cosa non sempre possibile e che rende meno portabile il codice.

C#

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int GetLongPathName(string path, StringBuilder longPath, int longPathLength);
…
if(str.Contains("~"))
{
StringBuilder sb = new StringBuilder(260);
if(GetLongPathName(str, sb, sb.Capacity) == 0)
return str;
return sb.ToString();
}

VB

<DllImport("kernel32.dll", CharSet:=CharSet.Auto, SetLastError:=True)> _
Public Shared Function 
GetLongPathName(ByVal path As String, ByVal longPath As StringBuilder, ByVal longPathLength As Integer) 
As Integer
End Function
 …
If (str.Contains("~")) Then
    Dim sb As New StringBuilder(260)
    If (GetLongPathName(str, sb, sb.Capacity) = 0) Then Return str
    Return sb.ToString()
End If

Se poi si vuole recuperare il solo nome del file senza la cartella, è importante non usare Path.GetFileName in quanto non rimuove ad esempio il punto finale dal file “NomeFileLungo.txt.”. Il punto finale invece viene rimosso da Path.GetFullPath quindi è importante prima rendere in forma canonica la path completa e poi ricavare il solo nome del file. Ecco l’implementazione:

C#

string GetFileNameOnly(string str)
{
str = CheckFile(str);
if(str.Length == 0)
return str;
return Path.GetFileName(str);
}

VB

Public Function GetFileNameOnly(ByVal str As String) As String
    str = CheckFile(str)
    If str.Length = 0 Then Return str
    Return Path.GetFileName(str)
End Function

I vantaggi della canonicalizzazione non sono solo di sicurezza ma anche per ovvia comodità. Dovendo custodire un nome di file è sempre bene usare FileInfo (o DirectoryInfo per le cartelle) ma sempre dopo averle canonicalizzate.