Binding Radio buttons to an Enum


Essendo una domanda ricorrente credo sia il caso di bloggarla: “Se il mio ViewModel espone una proprietà enumerativa, come posso bindarla ad un insieme di radio buttons ?”.
La risposta sta nell’utilizzo di un converter.

Partiamo dal ViewModel:

   1: public enum Power {Low,Medium,High}
   2:  
   3:     public class TheViewModel
   4:     {        
   5:         public Power PowerLevel { get; set; }        
   6:     }

ora creiamo un converter che sfruttando la possibilità di ricevere un parametro ritorna true quando il valore della proprietà coincide col parametro passatogli:

   1: public class PowerToBooleanConverter : IValueConverter
   2:     {
   3:         public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
   4:         {
   5:             Power power = (Power)value;
   6:             Power param = (Power)Enum.Parse(typeof(Power), parameter.ToString());
   7:             return (power == param);
   8:         }
   9:  
  10:         public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
  11:         {
  12:             Power valueToPush = (Power)Enum.Parse(typeof(Power), parameter.ToString());
  13:             return valueToPush;
  14:         }
  15:     }

A questo punto usiamo il ViewModel e relativo converter nello XAML:

   1: <Window x:Class="WPFToEnum.Window1"
   2:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   3:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:     xmlns:this="clr-namespace:WPFToEnum"
   5:     Title="Window1" Height="300" Width="300">
   6:     <Window.Resources>
   7:         <this:TheViewModel x:Key="tvm" PowerLevel="Medium" />
   8:         <this:PowerToBooleanConverter x:Key="converter" />
   9:     </Window.Resources>
  10:     <StackPanel DataContext="{StaticResource tvm}">
  11:         <RadioButton IsChecked="{Binding PowerLevel, Converter={StaticResource converter}, ConverterParameter=Low}">Low</RadioButton>
  12:         <RadioButton IsChecked="{Binding PowerLevel, Converter={StaticResource converter}, ConverterParameter=Medium}">Medium</RadioButton>
  13:         <RadioButton IsChecked="{Binding PowerLevel, Converter={StaticResource converter}, ConverterParameter=High}">High</RadioButton>
  14:     </StackPanel>
  15: </Window>

e il gioco è fatto… smile_regular

author: Corrado Cavalli | posted @ venerdì 3 luglio 2009 12.14 | Feedback (1)

DataContext Inheritance


Quando in una settimana due persone pongono lo stesso quesito è sintomo che quel particolare concetto non è ben chiaro o semplicemente non è stato spiegato correttamente. La domanda è: “Usando Model View ViewModel voglio creare una listbox con i vari elementi e un pulsante che mi permetta di cancellare quel determinato elemento” in pratica qualcosa tipo:

image

Normalmente il ViewModel che viene associato alla Window espone il comando di cancellazione della persona e la collezione di elementi da elencare, in breve qualcosa tipo:

   1: public class ViewModel : ViewModelBase
   2:     {
   3:         public ViewModel()
   4:         {
   5:             this.DeletePersonCommand = new RelayCommand<object>(o =>
   6:             {
   7:                 ICollectionView view = CollectionViewSource.GetDefaultView(this.Persons);
   8:                 this.Persons.Remove((Person)view.CurrentItem);
   9:             });
  10:  
  11:             //Creates some persons...
  12:             this.Persons = new ObservableCollection<Person>()
  13:             {
  14:                 new Person(){ FirstName="Corrado", LastName="Cavalli"},
  15:                 new Person(){ FirstName="Laurent", LastName="Bugnion"},
  16:                 new Person(){ FirstName="Marlon", LastName="Grech"},
  17:                 new Person(){ FirstName="Josh", LastName="Smith"}
  18:             };
  19:         }
  20:  
  21:  
  22:         private ObservableCollection<Person> persons = new ObservableCollection<Person>();
  23:  
  24:         /// <summary>
  25:         /// Gets the Persons property.
  26:         /// Changes to that property's value raise the PropertyChanged event. 
  27:         /// This property's value is broadcasted by the Messenger's default instance when it changes.
  28:         /// </summary>
  29:         public ObservableCollection<Person> Persons
  30:         {
  31:             get
  32:             {
  33:                 return this.persons;
  34:             }
  35:  
  36:             private set
  37:             {
  38:                 if (this.persons != value)
  39:                 {
  40:                     this.persons = value;
  41:                     RaisePropertyChanged("Persons");
  42:                 }
  43:             }
  44:         }        
  45:  
  46:         public RelayCommand<object> DeletePersonCommand { get; private set; }
  47:  
  48:     }

mentre la UI è definita secondo questo XAML:

   1: <Window x:Class="LaurentToolkit.MainWindow"
   2:         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   3:         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
   5:         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
   6:         mc:Ignorable="d"
   7:         Height="355"
   8:         Width="531"
   9:         DataContext="{Binding Main, Source={StaticResource Locator}}"
  10:         Title="{Binding WindowTitle}"
  11:         >
  12:     <Window.Resources>
  13:         <DataTemplate x:Key="PersonTemplate">
  14:             <StackPanel >
  15:             <TextBlock Text="{Binding FirstName, Mode=Default}" />
  16:             <TextBlock Text="{Binding LastName, Mode=Default}" />
  17:             <Button Content="Delete" 
  18:                     Command="{Binding DeletePersonCommand}" />
  19:             </StackPanel>
  20:         </DataTemplate>
  21:     </Window.Resources>
  22:     <Grid>
  23:         <Button HorizontalAlignment="Right" Margin="0,109,48,0" Width="72" Content="Close" Command="{Binding QuitCommand, Mode=Default}" Height="40" VerticalAlignment="Top" />
  24:         <ListBox HorizontalAlignment="Left" Margin="33,12,0,24" Width="173" ItemsSource="{Binding Persons, Mode=Default}" ItemTemplate="{DynamicResource PersonTemplate}"/>
  25:     </Grid>
  26: </Window>

Eseguendo il tutto, premendo il pulsante il comando DeletePersonCommand non viene invocato e il motivo è facilmente identificabile dando un occhio alla output window di Visual Studio che inesorabilemente riporta:

image

indicando chiaramente che il meccanismo di binding si aspetta di trovare la proprietà nell’oggetto associato al DataTemplate il quale rappresenta a tutti gli effetti il DataContext associato a ogni elemento della listbox.
Per far funzionare il tutto è necessario risalire al DataContext “principale” ereditato dai vari controlli inclusa la listbox, per far questo ci sono svariate possibilità, la più immediata, sebbene la sintassi da utilizzare non lo sia affatto, è quella di utilizzare un Binding relativo, risalendo il logical tree fino ad incontrare la listbox che contiene le istanze dei vari DataTemplates.

Modifichiamo il DataTemplate in:

   1: <DataTemplate x:Key="PersonTemplate">
   2:             <StackPanel >
   3:             <TextBlock Text="{Binding FirstName, Mode=Default}" />
   4:             <TextBlock Text="{Binding LastName, Mode=Default}" />
   5:             <Button Content="Delete" 
   6:                     Command="{Binding RelativeSource={RelativeSource FindAncestor, 
   7:                               AncestorLevel=1, 
   8:                               AncestorType={x:Type ListBox}}, 
   9:                               Path=DataContext.DeletePersonCommand}" />
  10:             </StackPanel>
  11:         </DataTemplate>

e il tutto funziona egregiamente.

image

 

 

 

Quello che abbiamo realizzato mediante RelativeSource è ‘risalire’ la gerarchia di elementi fino a raggiungere la listbox per poi usare come sorgente di binding la properietà DeletePersonCommand che è raggiungibile attraverso la proprietà DataContext della listbox la quale, grazie al DataContext inheritance, è stato valorizzato con l’istanza del ViewModel associato alla Window.

 

 

 

 

 

 

Questo approccio non è compatibile con Silverlight in quanto sebbene la versione 3.0 supporti RelativeSource questa è limitata alle opzioni Self e TemplatedParent, una possibile soluzione è quella di dare un nome alla listbox e usare un altra novità presente nella versione 3.0 ovvero ElementName, in pratica qualcosa tipo:

   1: <Window.Resources>
   2:         <DataTemplate x:Key="PersonTemplate">
   3:             <StackPanel >
   4:             <TextBlock Text="{Binding FirstName, Mode=Default}" />
   5:             <TextBlock Text="{Binding LastName, Mode=Default}" />
   6:             <Button Content="Delete" 
   7:                     Command="{Binding ElementName=lb, Path=DataContext.DeletePersonCommand}" />
   8:             </StackPanel>
   9:         </DataTemplate>
  10:     </Window.Resources>
  11:     <Grid>
  12:         <Button HorizontalAlignment="Right" Margin="0,109,48,0" Width="72" Content="Close" Command="{Binding QuitCommand, Mode=Default}" Height="40" VerticalAlignment="Top" />
  13:         <ListBox x:Name="lb" HorizontalAlignment="Left" Margin="33,12,0,24" Width="173" ItemsSource="{Binding Persons, Mode=Default}" ItemTemplate="{DynamicResource PersonTemplate}"/>
  14:     </Grid>
Technorati Tags: ,,

author: Corrado Cavalli | posted @ mercoledì 1 luglio 2009 19.07 | Feedback (0)

WPF Localization guidance


Un interessante whitepaper riguardante tutto quello che c’è da sapere sulla localizzazione in WPF: http://wpflocalization.codeplex.com

Technorati Tags: ,

author: Corrado Cavalli | posted @ mercoledì 1 luglio 2009 6.47 | Feedback (0)

UGIdotNET @ Predappio


Complice una giornata di consulenza c/o DentalTrey con i ragazzi che ci hanno ‘coccolato’ prima/durante/dopo il Workshop e che ormai ‘dominano’ WPF con estrema tranquillità sono rientrato da Predappio ieri sera ed ora sono qui a dire la mia sul recente workshop.
Sinceramente dire che ero via ‘per lavoro’ mi fa sentire un pò in colpa in quanto è stata a tutti gli effetti una piacevole gita fuori porta e un altra occasione per incontrare amici e colleghi di Managed Designs.
Sulla location non dico nulla in quanto altri ne hanno già parlato a sufficienza (Nicolò è stato per l’occasione il paparazzo della manifestazione) colgo solo l’occasione per ringraziare Michele, Riccardo e tutti gli altri per averci fatto sentire in evidente “imbarazzo” per cotanta ospitabilità e quanti sono intervenuti, parecchi facendo un sacco di chilometri, per venirci a sentire. Un ringraziamento personale ai ‘disgraziati’ che sono stati a sentirmi parlare di Silverlight 3.0 per 1h e 45m. smile_omg
Dai commenti sembra che l’agenda sia piaciuta, anzi che molti si sarebbero volentieri ‘sdoppiati’ per seguire entrambe le sessioni.
L’avvenimento è stato, ovviamente, la sessione di Raf.
Non potendo assistere essendo la sua sessione in parallelo alla mia Raffaele ci ha strabiliato privatamente domenica sera in hotel, per quelli che non hanno partecipato ecco ‘live ‘n’ full-effect’ i video di SurfRAF e OscilloRAF (sorry per la qualità ma è stata ripresa col telefonino…)

SurfRAF: http://www.facebook.com/video/video.php?v=97787756742&saved#/video/video.php?v=97787756742&ref=mf

OscilloRAF: http://www.facebook.com/video/video.php?v=97787756742&saved#/video/video.php?v=97787066742&ref=mf

Technorati Tags:

author: Corrado Cavalli | posted @ mercoledì 10 giugno 2009 11.50 | Feedback (1)

Tutte le differenze tra WPF e Silverlight


Se siete sviluppatori WPF e volete iniziare a sviluppare su Silverlight e/o viceversa, sicuramente vi verrà utile questo documento che elenca in modo dettagliato (69 pagine) tutte le differenze tra le due tecnologie.
Lo trovate qui: http://wpfslguidance.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=28278

Technorati Tags:

author: Corrado Cavalli | posted @ mercoledì 10 giugno 2009 10.47 | Feedback (2)

UI Technologies for Windows Embedded CE


Finalmente WPF/SL hanno raggiunto un area dove la necessità di avere user interfaces di ‘impatto’ è più che mai sentita: Windows CE.
Date un occhiata a questo video: User Interface Technologies for Windows Embedded CE

Technorati Tags: ,

author: Corrado Cavalli | posted @ giovedì 4 giugno 2009 6.57 | Feedback (0)

Silverlight 3 launch on July 10th


Silverlight 3 and Expression Studio 3 launching July 10

 

 

 

 

A quanto pare il 10 luglio verrà rilasciata la RTM  ci sarà il lancio di Silverlight3 e della nuova suite Expression…

Mi hanno fatto notare che “lancio” <> RTM ovvero: il compleanno va festeggiato prima dell’effettiva data di nascita. smile_regular

Technorati Tags:

author: Corrado Cavalli | posted @ giovedì 28 maggio 2009 22.51 | Feedback (0)

Silverlight3: DataForm control


Credo abbiate intuito che Microsoft sta spingendo molto sul presentare Silverlight3 come la piattaforma per lo sviluppo di RIA applications puntando molto sulle LOB (Line Of Business) Applications, i nuovi .NET RIA Services sono solo un esempio in questa direzione.
Oltre alla mia feature preferita (ChildWindow) in Silverlight3 sono stati aggiunti una serie di controlli nati con lo scopo di alleggerire la parte più noiosa delle LOB, ovvero la presentazione dei dati e relativa interazione con l’utente, tra questi il controllo DataForm.

Supponiamo di avere una classe Person con le classiche proprietà FirstName, LastName etc… e di volerne editare le proprietà.
In Silverlight2, sebbene banale, c’è un pò di lavoro da fare via XAML/VS2010/Blend, in Silverlight3 dopo avere impostato un riferimento a: System.Windows.Controls.Data, System.ComponentModel e System.Data.Annotations e avere trascinato il controllo DataForm nella pagina Silverlight non dobbiamo fare altro che scrivere:

   1: public partial class MainPage : UserControl
   2: {
   3:     public MainPage()
   4:     {
   5:         InitializeComponent();
   6:         DataForm1.CurrentItem = this.CreatePerson();
   7:     }
   8:  
   9:     private Person CreatePerson()
  10:     {
  11:         return new Person() {    FirstName = "Gianfranco", 
  12:                                         LastName = "Rossi", 
  13:                                         Role=Role.Tester,
  14:                                         BirthDate = DateTime.Parse("08/06/1981"), 
  15:                                         EMail = "gr@stupidsoft.it" 
  16:                                     };
  17:     }

per ottenere questo risultato:

image

 

 

Ovviamente quello che vedete è la rappresentazione di default, decorando person con una serie di attributi (la lista è immensa) è possibile personalizzare all’inverosimile le funzionalità del DataForm.

 

 

Andiamo a decorare la classe Person con gli attributi che vedete di seguito:

 

   1: [CustomValidation(typeof(PersonValidator), "ValidatePerson")]
   2: public class Person :IEditableObject
   3: {
   4:    
   5:     ...Fields...
   6:  
   7:     [Display(Name = "Nome", Order = 0, Description = "Nome utente")]
   8:     [Required(ErrorMessage = "Name is mandatory")]
   9:     [StringLength(10)]
  10:     public string FirstName
  11:     {
  12:         get { return firstName; }
  13:         set
  14:         {
  15:             if (!string.IsNullOrEmpty(value) && Char.IsNumber(value.ToCharArray()[0]))
  16:                 throw new ArgumentException("Name can't start with a number");
  17:             firstName = value;
  18:         }
  19:     }
  20:  
  21:     [Display(Name = "Cognome", Order = 1, Description = "Cognome utente")]
  22:     [Required(ErrorMessage = "Last Name is mandatory")]
  23:     public string LastName
  24:     {
  25:         get { return lastName; }
  26:         set { lastName = value; }
  27:     }
  28:  
  29:     [Display(Name = "Nato il", Order = 2, GroupName = "GeneralInfo")]
  30:     [Range(typeof(DateTime), "01/01/1970", "01/01/1990", ErrorMessage = "Date is out of range")]        
  31:     public DateTime BirthDate
  32:     {
  33:         get { return birthDate; }
  34:         set { birthDate = value; }
  35:     }
  36:  
  37:     [Bindable(true, BindingDirection.TwoWay)]
  38:     [Display(Name = "Email", Order = 3, GroupName = "GeneralInfo")]
  39:     [RegularExpression(@"^([0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,9})$", ErrorMessage = "Invalid Email")]
  40:     public string EMail
  41:     {
  42:         get { return eMail; }
  43:         set { eMail = value; }
  44:     }
  45:  
  46:     [Bindable(true)]
  47:     [Display(Name = "Ruolo", Order = 4, GroupName = "GeneralInfo")]
  48:     [CustomValidation(typeof(PersonValidator), "ValidateRole")]
  49:     public Role Role
  50:     {
  51:         get { return role; }
  52:         set { role = value; }
  53:     }
  54:  
  55:     #region IEditableObject Members
  56:     ...
  57:     #endregion
  58: }
 
quello che otterremo e questo risultato:

image Uno sguardo veloce agli attributi più comuni:

  • Bindable: Indica se il campo deve apparire oppure no, cosi come se deve essere Read-Only
    Display: Determina la posizione del campo nella  UI e il relativo nome (localizzabile)
    Required: Indica che il campo è obbligatorio (il nome del campo viene visualizzato in grassetto)
    StringLength: Massima lunghezza ammessa.
    Range: Valida il range ammesso.
    RegularExpression: Valida il campo usando una regular expression.
    CustomValidator: Permette di validare un campo o l’intera istanza usando il nostro codice.

Notate come il controllo visualizzi sia l’elenco dei campi non validati che eventuali eccezioni generate durante la valorizzazione del campo, e come se, come nel nostro caso, il tipo implementa IEditableObject, un pulsante Cancel per annullare l’editing che si attiva premendo il pulsante in alto a dx oppure, se la proprietà AutoEdit del controllo è True cliccando semplicemente su un campo.

Ok: Ma se ho un insieme di elementi, ad esempio una collezione di oggetti Persons?
No problem, basta associare la collezione alla proprietà ItemSource (via databinding oppure via codice) per ottenere questo risultato:

image

 

 

 

Notate come è possibile scorrere, editare, cancellare o inserire elementi usando i pulsanti in alto.

 

 

 

 

Ovviamente se quello che potete ottenere con gli attributi non vi basta, potete creare dei DataTemplate custom e associarli alle properietà HeaderTemplate, EditTemplate e DisplayTemplate personalizzando a proprio piacimento la user interface senza modificarne il comportamento.

imageDisplay Mode image  Edit Mode (notate come i nomi dei pulsanti sono personalizzabili)

Vi assicuro che quelle elencate sono solo alcune delle opzioni messe a disposizione dal controllo DataForm, le possibilità di personalizzazione sono veramente infinite.

author: Corrado Cavalli | posted @ venerdì 22 maggio 2009 17.13 | Feedback (1)

Silverlight3: Exception propagation


In Silverlight2, dopo avere creato il nostro bel servizio WCF che genera una serie di eccezioni (ovviamente di tipo FaultException/FaultException<T>) quando c’è qualcosa che non va, è triste scoprire che di queste eccezioni non ce ne facciamo nulla in quanto non verranno mai intercettate dalla nostra applicazione Silverlight per una lunga serie di motivi.

Fortunatamente questa è acqua passata in quanto nella versione 3 le eccezioni vengono correttamente gestite dal plug-in a patto che venga iniettato del codice per aggirare il problema principale: Le eccezioni vengono ritornate con un HTTP Response Code=500 e quindi vengono intercettate dal browser non raggiungendo mai il plug-in Silverlight.

Il trick consiste nel utilizzare un Message Inspector che trasformi un HttpResponseCode da 500 a 200 in modo che la risposta possa arrivare al plug-in Silverlight.
Il codice è disponibile qui: http://code.msdn.microsoft.com/silverlightws/Release/ProjectReleases.aspx?ReleaseId=1660  mentre un estratto di codice per il nostro caso è quello che segue (giusto per capire quali sono i namespaces che ho definito)

   1: namespace CustomBehaviors
   2: {
   3:     public class SilverlightFaultBehavior : BehaviorExtensionElement, IEndpointBehavior
   4:     {
   5:         ...
   6:     }
   7: }

A questo punto dobbiamo iniettare il custom behavior via Web.config:

   1: <system.serviceModel>
   2:     <extensions>
   3:         <behaviorExtensions>
   4:             <!--Defines custom message inspector-->
   5:             <add name="silverlightFaults"
   6:                   type="CustomBehaviors.SilverlightFaultBehavior, CustomBehaviors, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>                
   7:         </behaviorExtensions>
   8:     </extensions>
   9:  
  10:     <behaviors>
  11:         <endpointBehaviors>
  12:             <!--Custom behavior for message inspection-->
  13:             <behavior name="silverlightFaultBehavior">
  14:                 <silverlightFaults/>
  15:             </behavior>
  16:         </endpointBehaviors>
  17:         <serviceBehaviors>
  18:             <behavior name="YouBook.Web.YouBookDataServiceBehavior">
  19:                 <serviceMetadata httpGetEnabled="true"/>
  20:                 <serviceDebug includeExceptionDetailInFaults="false"/>
  21:             </behavior>
  22:         </serviceBehaviors>
  23:     </behaviors>
  24:     <bindings>
  25:         <customBinding>
  26:             <binding name="CustomBinding">
  27:                 <binaryMessageEncoding>
  28:                     <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="2147483647" maxNameTableCharCount="2147483647"/>
  29:                 </binaryMessageEncoding>
  30:                 <httpTransport maxReceivedMessageSize="2147483647" maxBufferSize="2147483647"/>
  31:             </binding>
  32:         </customBinding>
  33:     </bindings>
  34:     <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
  35:     <services>
  36:         <service behaviorConfiguration="YouBook.Web.YouBookDataServiceBehavior" name="YouBook.Web.YouBookDataService">
  37:             <endpoint address="" 
  38:                          binding="customBinding" 
  39:                          bindingConfiguration="CustomBinding" 
  40:                          contract="YouBook.Web.IYouBookDataService"
  41:                          behaviorConfiguration="silverlightFaultBehavior"
  42:                          />
  43:             <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
  44:         </service>
  45:     </services>
  46: </system.serviceModel>

That’s all!

Supponendo di generare una FaultException lato server in questo modo:

   1: try
   2: {
   3:     YouBookDataContext db = new YouBookDataContext();
   4:     User loggedUser = db.Users.FirstOrDefault(user => user.UserName == userName && user.Password == password);                    
   5:     return loggedUser;
   6: }
   7: catch (Exception ex)
   8: {
   9:     FaultReason reason = new FaultReason(ex.Message);
  10:     throw new FaultException(reason);
  11: }

Lato client possiamo, finalmente, gestire questo caso usando un banalissimo blocco try/catch.

   1: private void proxy_LoginCompleted(object sender, LoginCompletedEventArgs e)
   2: {
   3:     if (e.Error != null)
   4:     {
   5:         FaultException<LoginFault> loginException = e.Error as FaultException<LoginFault>;
   6:  
   7:         if (loginException != null)
   8:         {
   9:             UserDialog dialog = new UserDialog("Login Failed", loginException.Detail.Reason);
  10:             dialog.Show();
  11:         }
  12:         return;
  13:     }
  14:     ...
  15:  
  16: }

Speriamo che nella RTM semplifichino la gestione del cambio di HTTP Response Code cosi che intercettare le eccezioni lato server sia ancora più facile.

author: Corrado Cavalli | posted @ venerdì 22 maggio 2009 16.14 | Feedback (0)

WPF 4.0 Offline documentation


Volete tutta la documentazione MSDN relativa a WPF aggiornata alla 4.0 Beta1 in un unico file chm?
La trovate qui: http://cospire.com/wpf_docs.zip (108 MB)

author: Corrado Cavalli | posted @ giovedì 21 maggio 2009 12.44 | Feedback (1)