Generare immagini renderizzando contenuto XAML

30. March 2009

Qualche tempo fa, io e il mio collega Fabio, abbiamo avuto l’idea di generare immagini renderizzando contenuto XAML ed utilizzare queste immagini all’interno di applicazioni web. Da questa idea è nata una libreria costituita da un “motore” che renderizza il contenuto di un FrameworkElement generando un’immagine ed un HttpHandler, attraverso il quale utilizzare questo motore all’interno di applicazione ASP.NET.

La generazione dell’immagine a partire dal FrameworkElement viene eseguita dal metodo RenderToTargetBitmap della classe XamlRender contenuta nella libreria e avviene in questo modo:

  1. viene forzato il calcolo del layout dell’elemento attraverso i metodi Measure(), Arrange() e UpdateLayout():
    element.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
    element.Arrange(new Rect(element.DesiredSize));
    element.UpdateLayout();
  2. attraverso un oggetto DrawingVisual, viene disegnato un rettangolo riempito con un VisualBrush ottenuto dal FrameworkElement (per default la dimensione del rettangolo è quella dell’elemento stesso ma è possibile passare la dimensione e il tipo di stretch attraverso i parametri “size” e “stretchMode”):
    DrawingVisual dvisual = new DrawingVisual();
    using (DrawingContext context = dvisual.RenderOpen())
    {
        VisualBrush brush = new VisualBrush(element);
        brush.Stretch = (size.HasValue && stretchMode.HasValue) ? stretchMode.Value : Stretch.Uniform;
        context.DrawRectangle(brush, null, new Rect(new Point(), (size.HasValue) ? size.Value : element.RenderSize));
    }
  3. l’oggetto DrawingVisual creato viene renderizzato su un RenderTargetBitmap:
    RenderTargetBitmap renderBitmap = new RenderTargetBitmap(
        (size.HasValue) ? (int)size.Value.Width : (int)element.RenderSize.Width,
        (size.HasValue) ? (int)size.Value.Height : (int)element.RenderSize.Height,
        dpiX,
        dpiY,
        pixelFormat);
    renderBitmap.Render(dvisual);
  4. viene generato un MemoryStream dell’immagine codificando l’oggetto RenderTargetBitmap (gli encoding possibili sono Gif, Png, Jpeg, Bitmap, Tiff e Wmp):
    BitmapEncoder encoder = JpegBitmapEncoder(); // sample with Jpeg encoder
    encoder.Frames.Add(BitmapFrame.Create(bitmap));
    Stream stream = new MemoryStream();
    encoder.Save(stream);

Il metodo RenderToTargetBitmap ha diversi overloads che permettono di specificare vari parametri. Uno di questi è il parametro “bindingParameters”, un dictionary che viene assegnato al DataContext del FrameworkElement che si vuole renderizzare e permette di passare dei parametri allo XAML stesso. Ad esempio è possibile avere uno XAML di questo tipo:

<Border xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        BorderBrush="{Binding Path=.[BorderBrush]}" 
        BorderThickness="{Binding Path=.[BorderThickness]}" 
        CornerRadius="{Binding Path=.[CornerRadius]}">
</Border>

La sintassi {Binding Path=.[chiave]} ci permette di bindare degli elementi di un dictionary alle proprietà degli elementi WPF. Il dictionary in questo caso potrebbe essere il seguente:

Dictionary<String, String> bindingParameters = new Dictionary<string, string>();
bindingParameters.Add("BorderBrush", "Red");
bindingParameters.Add("BorderThickness", "4");
bindingParameters.Add("CornerRadius", "10");

Nella sezione downloads è possibile scaricare questa libreria ed un’applicazione WPF di demo che utilizza un file XAML per generare immagini come queste:

test

test2 

Per vedere come utilizzare questa libreria all’interno di un’applicazione ASP.NET leggete il post di Fabio a questo indirizzo:

http://www.fabiofranzini.com/post/2009/03/27/WPF-al-servizio-del-Web.aspx

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

WPF , ,

Model-View-ViewModel

11. March 2009

Da qualche tempo si fa un gran parlare del pattern MVVM per lo sviluppo di applicazioni WPF. Si tratta di un’architettura che permette di sfruttare appieno la caratteristica di WPF di separazione del codice dall’interfaccia grafica e può essere utilizzato anche per applicazioni Silverlight.

Se siete interessati ad utilizzare questo pattern o semplicemente a capire su cosa si basa, vi consiglio di leggere questo articolo di Cristian Civera: “Architettura Model-View-ViewModel in un'applicazione WPF”.

Vi consiglio inoltre l’articolo “WPF Apps With The Model-View-ViewModel Design Pattern” pubblicato su MSDN Magazine e scritto da Josh Smith, e i seguenti screencasts, resi displonibili sempre da Josh Smith in questo post:

Implementing Model-View-ViewModel in WPF

Implementing Model-View-ViewModel in Silverlight

Implementing MVVM & Exploring UX Design Patterns

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

WPF, Silverlight , ,

UI Privileges Manager – Gestire i privilegi degli utenti in un’applicazione WPF

3. March 2009

Molto spesso, quando si sviluppa un’applicazione (soprattutto in ambito gestionale), è necessario gestire diversi livelli di accesso degli utenti, prevedendo la possibilità di “bloccare” alcune funzionalità a seconda dei privilegi che un utente ha o non ha. Il posto corretto per fare questa operazione sicuramente non è l’interfaccia utente, ma un qualche strato inferiore che gestisca la sicurezza, negando l’accesso alle operazioni non consentite. Ciò non toglie però che per rendere l’applicazione più elegante e user friendly è utile risolvere il problema anche a monte, evitando che l’utente possa accedere ad una funzionalità per la quale non ha accesso, rendendola inaccessibile dall’interfaccia utente. A questo scopo ho sviluppato una libreria per WPF che, sfruttando alcune caratteristiche della piattaforma (in particolare le Dependency Properties), permette di gestire i privilegi utente a livello di interfaccia senza la necessità di scrivere tonnellate di codice. Di seguito vado ad illustrare il funzionamento di questa libreria, che potete scaricare assieme ad un esempio nella pagina dei downloads.

Gestire la sicurezza attraverso le attached properties

Una delle tante caratteristiche di WPF che a mio parere rivoluzionano il modo di sviluppare le applicazioni rispetto a Windows Form, è quella delle attached properties, ovvero la possibilità di definire delle proprietà in una classe esterna che vanno ad arricchire le funzionalità degli elementi grafici, senza bisogno di estendere gli elementi stessi. In UIPrivilegesManager ho definito delle attached properties che estendono i controlli WPF per poter eseguire il check dei privilegi dell’utente e modificare lo stato o l’apparenza dei controlli in caso di fallimento del check. Attenzione, parlo di controlli non ha caso. In WPF, infatti, non tutti gli elementi che compongono l’interfaccia (che derivano dalla classe UIElement) sono accessibili all’utente, ma solo quelli che derivano dalla classe Control. Quindi è sensato che queste attached properties siano definite solo per gli oggetti di tipo Control.

Le quattro attached properties sono definite nella classe ControlBehaviors e sono:

  • RequiredPrivileges
  • CheckPrivilegesMode
  • CheckPrivilegesBehavior
  • CheckPrivilegesType

 

RequiredPrivileges

Si tratta della lista dei privilegi che l’utente deve possedere per poter interagire con quel controllo. Un privilegio è un’entità molto generica descritta semplicemente da un nome (Name) e può essere quindi una singola operazione, un gruppo, un ruolo, o altro, a seconda di come è definita la sicurezza nell’applicazione.

   1: <Button ...>
   2:     <pm:ControlBehaviors.RequiredPrivileges>
   3:         <pm:PrivilegeCollection>
   4:             <pm:Privilege Name="..." />
   5:             ...
   6:         </pm:PrivilegeCollection>
   7:     </pm:ControlBehaviors.RequiredPrivileges>
   8: </Button>

pm è il nome del namespace importato nello xaml in questo modo:

   1: xmlns:pm="clr-namespace:UIPrivilegesManager;assembly=UIPrivilegesManager"

CheckPrivilegesMode

Descrive il modo in cui viene eseguito il check dei privilegi e può prendere due valori: “All” e “Any”. Nel primo caso significa che l’utente deve avere tutti i privilegi richiesti dal controllo, mentre nel secondo caso ne deve avere almeno uno. Il valore di default è “All”.

   1: <Button pm:ControlBehaviors.CheckPrivilegesMode="All" ...>
   2:     ...
   3: </Button>

CheckPrivilegesBehavior

Questa proprietà definisce la modalità con la quale il manager gestisce il controllo dei privilegi per quel controllo. Può avere uno dei tre valori:

  • DisableControl: in caso l’utente non abbia i privilegi richiesti il controllo viene disabilitato (IsEnabled = false)
  • HideControl: in caso l’utente non abbia i privilegi richiesti il controllo viene nascosto (Visibility = Collapsed)
  • Custom: il manager lancia semplicemente il metodo CheckPrivileges della classe impostata con la proprietà CheckPrivilegesType

Il valore di default è “DisableControl”.

   1: <Button pm:ControlBehaviors.CheckPrivilegesBehavior="DisableControl" ...>
   2:     ...
   3: </Button>

CheckPrivilegesType

Deve essere settata quando la proprietà CheckPrivilegesBehavior è impostata su “Custom”. Accetta un oggetto che implementa l’interfaccia ICheckPrivileges che fornisce il metodo CheckPrivileges (un esempio di classe custom lo trovate nella demo):

   1: public interface ICheckPrivileges
   2: {
   3:     void CheckPrivileges(Control control, PrivilegeCollection userPrivileges, PrivilegeCollection requiredPrivileges);
   4: }

Se la proprietà non è impostata il manager solleva un’eccezione di tipo PrivilegesManagerException.

   1: ...
   2: <local:CustomCheckPrivileges x:Key="CustomCheckPrivileges " />
   3: ...
   4: <Button pm:ControlBehaviors.CheckPrivilegesBehavior="Custom"
   5:         pm:ControlBehaviors.CheckPrivilegesType="{StaticResource CustomCheckPrivileges}">
   6:     ...
   7: </Button>         

Control.Loaded

In fase di registrazione dell’attached property RequiredPrivileges ho definito nei metadata la PropertyChangedCallback che OnRequiredPrivilegesChanged. In questa callback, vado ad abbonarmi all’evento Loaded del controllo, che è il punto di ingresso in cui viene legato il controllo al PrivilegesManager.

   1: private static void control_Loaded(object sender, RoutedEventArgs e)
   2: {
   3:     Control c = sender as Control;
   4:     switch (GetCheckPrivilegesBehavior(c))
   5:     {
   6:         case CheckPrivilegesBehavior.DisableControl:
   7:             {
   8:                 PrivilegesManager.ControlsToDisable.Add(c);
   9:                 c.InvalidateProperty(Control.IsEnabledProperty);
  10:                 break;
  11:             }
  12:         case CheckPrivilegesBehavior.HideControl:
  13:             {
  14:                 PrivilegesManager.ControlsToHide.Add(c);
  15:                 c.InvalidateProperty(Control.VisibilityProperty);
  16:                 break;
  17:             }
  18:         case CheckPrivilegesBehavior.Custom:
  19:             {
  20:                 PrivilegesManager.RunCustomCheckMethod(c);
  21:                 break;
  22:             }
  23:     }
  24: }

Come si può vedere dal codice, nel metodo control_Loaded eseguo uno switch sulla proprietà CheckPrivilegesBehavior e delego alla classe PrivilegesManager il compito di eseguire il check dei privilegi.

DisableControl e HideControl

Vale la pena di mostrare il funzionamento di questi due behavior perché non è così banale come potrebbe sembrare. Partiamo dalla cosa ovvia: “lavorano” rispettivamente sulle proprietà IsEnabled e Visibility del controllo. Il check viene eseguito una prima volta all’evento loaded del controllo (grazie al metodo InvalidateProperty(proprietà), vedi metodo control_Loaded) ma successivamente occorre tenere traccia se durante l’esecuzione dell’applicazione queste proprietà vengono modificate, magari via codice o perchè associate ad un binding, e in caso rilanciare il check. Ma entrambe queste proprietà sono dependency properties e quindi ci viene in aiuto la classe DependencyPropertyDescriptor, più precisamente la possibilità di settare attraverso questa classe la proprietà DesignerCoerceValueCallback. Questa callback viene lanciata ogni volta che viene modificato il valore della dependency property e fornisce la possibilità di forzare il valore della proprietà al valore desiderato PRIMA che la proprietà venga effettivamente settata. In questa callback possiamo quindi eseguire il check dei privilegi ogni volta che la proprietà sta per essere modificata e quindi forzarne il valore (a false, per la proprietà IsEnabled e a Collapsed per la proprietà Visibility). Ad esempio, per la proprietà IsEnabled (per la proprietà Visibility è esattamente la stessa cosa) abbiamo:

   1: ...
   2: DependencyPropertyDescriptor isEnabledPD = DependencyPropertyDescriptor.FromProperty(Control.IsEnabledProperty, typeof(Control));
   3: isEnabledPD.DesignerCoerceValueCallback = new CoerceValueCallback(CoerceIsEnabled);
   4: ...
   5: private static object CoerceIsEnabled(DependencyObject obj, object value)
   6: {
   7:     Control c = obj as Control;
   8:     if (c != null)
   9:     {
  10:         if (ControlsToDisable.Contains(c) && (bool)value)
  11:         {
  12:             return UserHasRequiredPrivileges(ControlBehaviors.GetRequiredPrivileges(c), ControlBehaviors.GetCheckPrivilegesMode(c));
  13:         }
  14:     }
  15:     return value;
  16: }

La callback DesignerCoerceValueCallback è generica per ogni controllo all’interno dell’applicazione quindi viene lanciata anche per i controlli per i quali non sono settati i privilegi richiesti. Per questo motivo ho memorizzato i controlli che hanno come behavior DisableControl o HideControl all’interno di due liste (ControlsToDisable e ControlsToHide) per poter eseguire i check solamente per i controlli corretti.

Le callback di coerce richiamano un metodo che esegue il check a seconda della modalità impostata sul controllo:

   1: private static bool UserHasRequiredPrivileges(PrivilegeCollection requiredPrivileges, CheckPrivilegesMode mode)
   2: {
   3:     switch (mode)
   4:     {
   5:         case CheckPrivilegesMode.All:
   6:             {
   7:                 return _userPrivileges.Intersect(requiredPrivileges).Count() == requiredPrivileges.Count;
   8:             }
   9:         case CheckPrivilegesMode.Any:
  10:             {
  11:                 return _userPrivileges.Intersect(requiredPrivileges).Count() > 0;
  12:             }
  13:     }
  14:     return false;
  15: }

Configurazione di UIPrivilegesManager

Fino a questo momento ho parlato di un confronto tra i privilegi richiesti da un controllo e quelli in possesso dell’utente. Ma come fa il manager ha sapere quali sono i privilegi dell’utente? Nell’applicazione è possibile definire una classe che implementi l’interfaccia IGetUserPrivileges, che fornisce il metodo GetUserPrivileges, attraverso il quale caricare i privilegi degli utenti:

   1: public interface IGetUserPrivileges
   2: {
   3:     PrivilegeCollection GetUserPrivileges();
   4: }

Questa classe deve essere specificata nel file di configurazione dell’applicazione, in una sezione ad hoc:

   1: <configuration>
   2:     <configSections>
   3:         ...
   4:         <section name="PrivilegesManager" type="UIPrivilegesManager.PrivilegesManagerConfiguration, UIPrivilegesManager" allowLocation="true" allowDefinition="Everywhere" />
   5:     </configSections>
   6:     ...
   7:     <PrivilegesManager EnableCheckPrivileges="True" LoadUserPrivilegesType="PrivilegesManagerSample.LoadUserPrivileges, PrivilegesManagerSample" />
   8: </configuration>

La sezione fornisce anche la proprietà EnableCheckPrivileges, di tipo booleano, che permette di disabilitare la sicurezza in modo semplice, utile in fase di sviluppo e di test (il valore di default è true).

Questa è una soluzione per configurare in modo semplice la libreria ma se per qualche ragione non è possibile utilizzare il file di configurazione (ad esempio se l’utente finale  purtroppo ha i privilegi di modificare il file e quindi disabilitare il check, cosa che in linea teorica non dovrebbe poter fare!), la classe PrivilegesManager fornisce un metodo statico pubblico per poter inizializzare i privilegi utente, da chiamare allo startup dell’applicazione:

   1: public partial class App : Application
   2: {
   3:     protected override void OnStartup(StartupEventArgs e)
   4:     {
   5:         base.OnStartup(e);
   6:  
   7:         PrivilegeCollection privileges = ...
   8:         UIPrivilegesManager.PrivilegesManager.SetUserPrivileges(privileges);
   9:     }
  10: }

Provalo!

Potete scaricare i sorgenti e/o i binari qui. Assieme troverete una demo che mostra come utilizzare la libreria e fornisce anche un esempio di implementazione di una classe custom per il check dei privilegi su controlli di tipo Button, a mio parere molto interessante perché permette di avere un comportamento simile a quello di UAC, ovvero di chiedere credenziali con privilegi più alti quando l’utente corrente non ha i privilegi richiesti dal bottone.

Currently rated 5.0 by 4 people

  • Currently 5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

WPF , , , , , ,

Auto-size delle colonne di una ListView

26. February 2009

In WPF il modo più semplice per visualizzare dei dati in una griglia è quello di utilizzare la ListView. Utilizzando questo controllo però mi sono scontrato più volte con il problema che le colonne non si adattano automaticamente al contenuto ma si costringe l’utente a ridimensionarle a mano. Di seguito propongo una soluzione a questo problema.

La proprietà Width della GridViewColumn, di tipo double, può essere settata ad un valore fisso oppure ad “Auto”; nel secondo caso la proprietà viene inizializzata al valore double.NaN e solamente la prima volta che viene settata la proprietà ItemsSource della ListView viene calcolata la larghezza della colonna (larghezza massima degli oggetti contenuti in quella colonna).

Per fare in modo che WPF ricalcoli la larghezza delle colonne possiamo quindi pensare di resettare la proprietà Width al valore double.NaN ogni volta che viene modificata l’ItemsSource della ListView, attraverso un metodo come questo:

   1: private void UpdateColumnsWidth(ListView list)
   2: {
   3:     GridView view = (GridView)list.View;
   4:     foreach (GridViewColumn col in view.Columns)
   5:     {
   6:         col.Width = 0;
   7:         col.Width = double.NaN;
   8:     }
   9: }

Per ottenere il comportamento desiderato dobbiamo lanciare questo metodo ogni volta che:

  • viene settata la proprietà ItemsSource
  • viene aggiunto o rimosso un elemento dalla lista

 

L’oggetto ListView non fornisce un evento che notifica la modifica del valore della proprietà ItemsSource ma, poiché si tratta di una DependencyProperty, è possibile ottenere tale notifica utilizzando il metodo AddValueChanged dell’oggetto DependencyPropertyDescriptor:

   1: ...
   2: DependencyPropertyDescriptor descriptor = DependencyPropertyDescriptor.FromProperty(ListView.ItemsSourceProperty, typeof(ListView));
   3: descriptor.AddValueChanged(lv, new EventHandler(OnItemsSourcePropertyChanged));
   4: ...
   5:  
   6: private void OnItemsSourcePropertyChanged(object sender, EventArgs e)
   7: {            
   8:     ListView lv = sender as ListView;
   9:     UpdateColumnsWidth(lv);
  10: }

Per avere la notifica quando viene aggiunto o rimosso un elemento, invece, è necessario che la lista che andiamo a settare come ItemsSource implementi l’interfaccia INotifyCollectionChanged (ad esempio, ObservableCollection<T>), che fornisce l’evento CollectionChanged:

   1: private void OnItemsSourcePropertyChanged(object sender, EventArgs e)
   2: {
   3:     ...
   4:  
   5:     INotifyCollectionChanged itemsSource = lv.ItemsSource as INotifyCollectionChanged;
   6:     if (itemsSource != null)
   7:     {
   8:         itemsSource.CollectionChanged += new NotifyCollectionChangedEventHandler(itemsSource_CollectionChanged);
   9:     }
  10: }
  11:  
  12: void itemsSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
  13: {
  14:     UpdateColumnsWidth(lv);
  15: }

A questo punto abbiamo ottenuto il comportamento desiderato. Per poter riutilizzare questo meccanismo senza dover riscrivere tutto questo codice per ogni ListView, andiamo a definire una Attached Property:

   1: public static readonly DependencyProperty IsEnableAutoSizeProperty =
   2:     DependencyProperty.RegisterAttached("IsEnableAutoSize",
   3:                                         typeof(bool),
   4:                                         typeof(ListViewColumnAutoSize),
   5:                                         new PropertyMetadata(false, new PropertyChangedCallback(IsEnableAutoSizePropertyChanged)));
   6:  
   7: public static bool GetIsEnableAutoSize(ListView lv)
   8: {
   9:     return (bool)lv.GetValue(IsEnableAutoSizeProperty);
  10: }
  11:  
  12: public static void SetIsEnableAutoSize(ListView lv, bool value)
  13: {
  14:     lv.SetValue(IsEnableAutoSizeProperty, value);
  15: }

In fase di registrazione dell’attached property, andiamo a specificare nei metadata un metodo di callback per gestire la cambiamento del valore della proprietà. In questo metodo andremo ad abbonarci (o a disabbonarci) alla modifica della proprietà ItemsSource della ListView utilizzando il DependencyPropertyDescriptor, come visto precedentemente:

   1: private static void IsEnableAutoSizePropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
   2: {
   3:     ListView lv = obj as ListView;
   4:     DependencyPropertyDescriptor descriptor = DependencyPropertyDescriptor.FromProperty(ListView.ItemsSourceProperty, typeof(ListView));
   5:     if ((bool)e.NewValue)
   6:     {
   7:         descriptor.AddValueChanged(lv, new EventHandler(OnItemsSourcePropertyChanged));
   8:     }
   9:     else
  10:     {
  11:         descriptor.RemoveValueChanged(lv, new EventHandler(OnItemsSourcePropertyChanged));
  12:     }
  13: }

 

Nel metodo OnItemsSourcePropertyChanged si andrà a lanciare il metodo UpdateColumnsWidth e ad abbonarsi all’evento CollectionChanged della lista sorgente, come visto in precedenza.

Una volta definita l’attached property è possibile settarla direttamente dallo XAML in questo modo:

   1: ...
   2: <ListView local:ListViewColumnsAutoSize.IsEnableAutoSize="True">
   3: ...

 

[Scarica l’esempio completo]

Currently rated 5.0 by 3 people

  • Currently 5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

WPF ,