People that have worked with Windows Presentation Foundation probably know the usefulness of the CollectionViewSource control. This control is simple to be configured and enables to easily filter, sort and group data items before binding them to an ItemsControl, without any need to change the underlying datasource. Many times we have to operate filtering on a datasource before displaying it in a listbox, or we need to sort the same data answering to a user request. Both this behaviors may be handled using the CollectionViewSource; filtering will raise an event asking to choice the items to show or to hide and we have simply to select the elements with the correct criteria. On the other side, the control can sort elements on one or more properties ascending or descending. If the target control support grouping we can also specify a grouping criteria and the control will operate seamless for us. But probably the better thing is that we can change sorting, filtering or grouping and the target control will be updated according to the new criteria.

Unfortunately Silverlight 2.0 does not have a CollectionViewSource but while exploring the capabilities of databinding I found that it is possible to build a control similar to the CollectionViewSource that support both filtering and sorting. There aren't controls supporting grouping in Silverlight 2.0 so it is unuseful to implement it in this control. Before getting to know how the control works it is better to understand how databinding works in Silverlight 2.0 so let me begin with a brief introduction to this argument.

Databinding in Silverlight 2.0

Databinding is a feature introduced many years ago so there is no need to explain what it is. Windows Presentation Foundation has taken this ancient tecnique to an high level of simplicity and Silverlight 2.0 implements a databinding capability very close to WPF with some little limitations.

The two main components of databinding are DataContext and the Binding markup extension. DataContext is a property, exposed by all the classes inheriting from FrameworkElement so it is available on all the controls, shapes, panels and other user interface elements. Through DataContext property we may specify a class instance that will be the source of databinding for the target element and for all his children. The DataContext will propagate to children elements until the end of the hierarchy or until another DataContext will be encountered. All the elements of the hierarchy may be binded with a property of the object that is associated to the DataContext. To bind the properties each other we have to use the Binding markup extension. In the simplest form the markup extension require that we specify the property of the datasource to bind to.

Another way to bind to a user interface control is using an ItemsControl. Connecting a datasource to its ItemsSource property will cause the collection to be displayed in the control repeating a DataTemplate for every item in the collection. The DataTemplate will be binded to each element throught the Binding markup extension itself. Here is an example showing both the tecniques applied.

   1: <UserControl x:Class="Elite.Silverlight.Samples.Databinding"
   2:     xmlns="http://schemas.microsoft.com/client/2007" 
   3:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
   4:     xmlns:d="clr-namespace:Elite.Silverlight.Samples"
   5:     Width="400" Height="300">
   6:     <UserControl.Resources>
   7:         <d:DataSource x:Key="data" />
   8:     </UserControl.Resources>
   9:     <Grid x:Name="LayoutRoot" DataContext="{StaticResource data}" Background="White">
  10:         <ItemsControl ItemsSource="{Binding Strings}">
  11:             <ItemsPanelTemplate>
  12:                 <DataTemplate>
  13:                     <TextBlock Text="{Binding}" />
  14:                 </DataTemplate>
  15:             </ItemsPanelTemplate>
  16:         </ItemsControl>
  17:     </Grid>
  18: </UserControl>

In this sample an instance of a class named DataSource has been referenced in the user control resources. This class has a property named "Strings" returning an IEnumerable<string>. The yielded strings is the data to be displayed in the ItemsControl. The DataContext property of the Grid is assigned to a reference of the DataSource class and the ItemsSource property is binded to the Strings property throught the Binding markup extension. The data collected from the datasource become the DataContext for the each item with the template specified in ItemsTemplatePanel so the Binding markup extension assign each string to the Text property of the TextBlock.

For the purpose of this article we don't need to deepen our knowledge of this aspect. Instead, we need to concentrate on another aspect. Databinding enable developers to bind properties of a control to properties of a data item and have them updated when the data item change. In the same way it is possible to bind a collection to a specific property and have the control updated when we add or remove items to the collection. This two behaviors came from two interfaces: INotifyPropertyChanged and INotifyCollectionChanged. Objects implementing one of these interfaces have to notify internal changes through an event. PropertyChanged notify changes to single properties and is useful for objects binded directly to control properties, instead CollectionChanged notify about adding or removal of items from a collection and it has to be used for controls binded to collections like an ItemsControl.

Silverlight 2.0 and WPF come with a generic collection ObservableCollection<T> that implements INotifyCollectionChanged interface. This class helps us to bind collection of objects to an ItemsControl. We may create a class, expose a property of this type and bind it to an ItemsControl. Then we can add or remove elements to the collection and them will be added to or removed from the ItemsControl seamless.

Thinking about our CollectionViewSource

It is now the time to think about how to implement our CollectionViewSource. We need to create the control as a filter that will be interposed between the datasource and the consuming control and it will operate notifying changes responding to user choices. So probably our control has to appear as a collection for  the control that will displays the data items. To notify changes we need to implement the INotifyCollectionChanged interface so we get the consuming control to update its appearance responding to changes to the filtering or sorting criteria. Watching to the control from the side of the datasource it has to expose a Source property that we'll connect to the underlying datasource we need to transform.

So let start creating the control. It will be called CollectionViewSource as expected and extends the Control class. We cannot create a simple class that does not implement any other base class because we need to get access to the DependencyProperty system. The Control class is tipically dedicated to elements that have a visual aspect, so probably it would be better we extend a class like DependencyObject for our control to work. Unfortunately we have to use the Control class because all the base classes from FrameworkElement to DependencyObject does not have a public constructor so we are unable to inherit from them. The Control class is not the best choice, but it is the only choice available.

   1: public class CollectionViewSource : Control, IEnumerable, INotifyCollectionChanged
   2: {
   3:     // implement the control... 
   4: }

The class has also to implement two interfaces: IEnumerable and INotifyCollectionChanged. The first interface is required to grant our control the capability to be consumed from an ItemsControl. Using the IEnumerable interface we get two goals. We can assign the CollectionViewSource directly to the ItemsSource property and we can use LINQ to Objects to yield resulting items to the consuming control. Taking advantage of LINQ to Objects will simplify our control structure and get our code to a better level of readability.

   1: #region IEnumerable Members
   2:  
   3: /// <summary>
   4: /// Returns an enumerator that iterates through a collection.
   5: /// </summary>
   6: /// <returns>
   7: /// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
   8: /// </returns>
   9: public IEnumerator GetEnumerator()
  10: {
  11:     var result = this.Source
  12:         .Cast<object>();
  13:  
  14:     return result.GetEnumerator();
  15: }
  16:  
  17: #endregion

For the filtering to work we have to expose an event that we'll call every time we have to filter the collection, one time for every item in the collection asking the user to choice the items to show. The simplest thing to do is to create a delegate, that we use as a Where clause lambda expression in the Linq query yielding items. So the delegate will be called by the query every time it needs to examine an element to decide if it has to be yielded or skipped.

   1: /// <summary>
   2: /// Occurs when [filter].
   3: /// </summary>
   4: public event EventHandler<FilterCollectionEventArgs> Filter;
   5:  
   6: /// <summary>
   7: /// Returns an enumerator that iterates through a collection.
   8: /// </summary>
   9: /// <returns>
  10: /// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
  11: /// </returns>
  12: public IEnumerator GetEnumerator()
  13: {
  14:     var result = this.Source
  15:         .Cast<object>()
  16:         .Where(FilterItem);
  17:  
  18:     return result.GetEnumerator();
  19: }
  20:  
  21: /// <summary>
  22: /// Filters the item.
  23: /// </summary>
  24: /// <param name="o">The o.</param>
  25: /// <returns></returns>
  26: private bool FilterItem(object o)
  27: {
  28:     FilterCollectionEventArgs args = new FilterCollectionEventArgs(o);
  29:     OnFilter(args);
  30:     return args.Accept;
  31: }
  32:  
  33: /// <summary>
  34: /// Raises the <see cref="E:Filter"/> event.
  35: /// </summary>
  36: /// <param name="e">The <see cref="Elite.Silverlight.FilterCollectionEventArgs"/> instance containing the event data.</param>
  37: protected virtual void OnFilter(FilterCollectionEventArgs e)
  38: {
  39:     EventHandler<FilterCollectionEventArgs> handler = Filter;
  40:  
  41:     if (handler != null)
  42:         handler(this, e);
  43: }

To configure the sorting behavior we have to expose a property that collect the sorting criteria in a way similar to the CollectionViewSource contained in the Windows Presentation Foundation. This require to create a SortDescriptionCollection class that will collect the SortDescription instances. Every SortDescription will require a PropertyName and a Direction property to tell the control wich property use for sorting and the direction of the sorting. All the names I've proposed come directly from the WPF control. Using this namig will grant our CollectionViewSource the maximum compatibility with the WPF control so we may use code snippet created for WPF in Silverlight 2.0.

We use the same LINQ to Objects expression adding OrderBy and ThenBy calls to the query if the sorting is enabled. The sorting will be called immediately after the Where clause.

   1: /// <summary>
   2: /// Returns an enumerator that iterates through a collection.
   3: /// </summary>
   4: /// <returns>
   5: /// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
   6: /// </returns>
   7: public IEnumerator GetEnumerator()
   8: {
   9:     var result = this.Source
  10:         .Cast<object>()
  11:         .Where(FilterItem);
  12:  
  13:     foreach (SortDescription sd in this.SortDescriptions)
  14:     {
  15:         if (sd.Direction == ListSortDirection.Ascending)
  16:             result = AppendOrderBy(result, sd.PropertyName);
  17:         else
  18:             result = AppendOrderByDescending(result, sd.PropertyName);
  19:     }
  20:  
  21:     return result.GetEnumerator();
  22: }
  23:  
  24: /// <summary>
  25: /// Appends the order by.
  26: /// </summary>
  27: /// <param name="result">The result.</param>
  28: /// <param name="propertyName">Name of the property.</param>
  29: /// <returns></returns>
  30: private IEnumerable<object> AppendOrderBy(IEnumerable<object> result, string propertyName)
  31: {
  32:     if (result is IOrderedEnumerable<object>)
  33:         return ((IOrderedEnumerable<object>)result).ThenBy(o => GetPropertyValue(o, propertyName));
  34:  
  35:     return result.OrderBy(o => GetPropertyValue(o, propertyName));
  36: }
  37:  
  38: /// <summary>
  39: /// Appends the order by descending.
  40: /// </summary>
  41: /// <param name="result">The result.</param>
  42: /// <param name="propertyName">Name of the property.</param>
  43: /// <returns></returns>
  44: private IEnumerable<object> AppendOrderByDescending(IEnumerable<object> result, string propertyName)
  45: {
  46:     if (result is IOrderedEnumerable<object>)
  47:         return ((IOrderedEnumerable<object>)result).ThenByDescending(o => GetPropertyValue(o, propertyName));
  48:  
  49:     return result.OrderByDescending(o => GetPropertyValue(o, propertyName));
  50: }

With the INotifyCollectionChanged interface we'll notify the control to update his aspect when changes occur. The only thing we have to take care is the way to notify events. The NotifyCollectionChangedEventArgs class we have to use to raise the CollectionChanged event require us to specify the kind of event we are raising. The constructor of this class need to specify a NotifyCollectionChangedAction enumeration parameter that define if we are updating the datasource for Add, Remove, Replace, Move or Reset. To get the ItemsControl updating we need to specify the NotifyCollectionChangedAction.Reset value because every other value does not have an effect on the ItemsControl. This behavior become from the implementation of the ItemsSource property. Inspecting this property with Lutz Roeder Reflector it will show us that it is wrapped with an internal private EnumerableCollectionView that will mask every other type of event. The drawback of this choice is that the target control will be updated entirely instead of handling only the added or removed items. I hope that a future implementation of this control will change this thing, but now we have to accept this behavior to get the CollectionViewSource working.

We have to handle two type of events that raise the CollectionChanged event. The first one is when the underliyng collection raise the same event. This is because if the datasource change we have to reflect this change to the user interface itself forwarding this event. So when the Source property will be assigned we have to hook up its CollectionChanged event (if the source itself implements the INotifyCollectionChanged). For this purpose we create the source property as a DependencyProperty to take advantage of the PropertyChangedCallback that will notify when the Source property change its value. When the value change we have to remove event from a previous instance before to hook the new instance to avoid memory leaks and unexpected results.

   1: public static readonly DependencyProperty SourceProperty =
   2:     DependencyProperty.Register("Source", typeof(IEnumerable), typeof(CollectionViewSource), new PropertyChangedCallback(SourceProperty_Changed));
   3:  
   4: /// <summary>
   5: /// Gets or sets the source.
   6: /// </summary>
   7: /// <value>The source.</value>
   8: public IEnumerable Source
   9: {
  10:     get { return (IEnumerable)this.GetValue(SourceProperty); }
  11:     set { this.SetValue(SourceProperty, value); }
  12: }
  13:  
  14: /// <summary>
  15: /// Sources the property_ changed.
  16: /// </summary>
  17: /// <param name="sender">The sender.</param>
  18: /// <param name="e">The <see cref="System.Windows.DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
  19: private static void SourceProperty_Changed(DependencyObject sender, DependencyPropertyChangedEventArgs e)
  20: {
  21:     CollectionViewSource cvs = sender as CollectionViewSource;
  22:  
  23:     if (cvs != null)
  24:     {
  25:         if (e.OldValue != null && e.OldValue is INotifyCollectionChanged)
  26:             ((INotifyCollectionChanged)e.OldValue).CollectionChanged -= new NotifyCollectionChangedEventHandler(cvs.CollectionViewSource_CollectionChanged);
  27:  
  28:         if (e.NewValue != null && e.NewValue is INotifyCollectionChanged)
  29:             ((INotifyCollectionChanged)e.NewValue).CollectionChanged += new NotifyCollectionChangedEventHandler(cvs.CollectionViewSource_CollectionChanged);
  30:     }
  31: }
  32:  
  33: /// <summary>
  34: /// Handles the CollectionChanged event of the CollectionViewSource control.
  35: /// </summary>
  36: /// <param name="sender">The source of the event.</param>
  37: /// <param name="e">The <see cref="System.Collections.Specialized.NotifyCollectionChangedEventArgs"/> instance containing the event data.</param>
  38: private void CollectionViewSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
  39: {
  40:     if (sender != null)
  41:         this.OnCollectionChanged(e);
  42: }
  43:  
  44: /// <summary>
  45: /// Raises the <see cref="E:CollectionChanged"/> event.
  46: /// </summary>
  47: /// <param name="e">The <see cref="System.Collections.Specialized.NotifyCollectionChangedEventArgs"/> instance containing the event data.</param>
  48: protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
  49: {
  50:     NotifyCollectionChangedEventHandler handler = CollectionChanged;
  51:  
  52:     if (handler != null)
  53:         handler(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
  54: }

Last but not least we have to expose a method to enable users to force the CollectionViewSource to refresh the criteria and notifying the changes if needed. This is the second type of event we have to handle to notify the collection change. If the user call this method we will assume that something has been changed in the criteria and we raise the CollectionChanged event.

   1: /// <summary>
   2: /// Refreshes this instance.
   3: /// </summary>
   4: public void Refresh()
   5: {
   6:     this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
   7: }

Using the CollectionViewSource control

Now that the control has been completed we have to build a sample project that use the CollectionViewSource. For this purpose I have created an application that read the rss feed of my weblog (http://blog.boschin.it) and display it in an ItemsControl. The interface have a TextBox that we use to filter the post by title and two radio buttons that decide if sort the items ascending or descending by the publish date. The application is very simple but its purpose is only to dimostrate how to use the CollectionViewSource control and how simple is to use and customize its behavior. Here is a brief markup defining the application interface:

   1: <UserControl x:Class="Silverlight.Page"
   2:     xmlns="http://schemas.microsoft.com/client/2007" 
   3:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
   4:     xmlns:s="clr-namespace:Silverlight" Loaded="UserControl_Loaded"
   5:     xmlns:e="clr-namespace:Elite.Silverlight;assembly=Elite.Silverlight"
   6:     xmlns:ed="clr-namespace:Elite.Silverlight.Data;assembly=Elite.Silverlight"
   7:     Width="400" Height="400">
   8:     <UserControl.Resources>
   9:         <s:DataSource x:Name="dataSource" />
  10:         <ed:CollectionViewSource x:Name="rssView" Source="{Binding Items, Source={StaticResource dataSource}}" Filter="rssView_Filter">
  11:             <CollectionViewSource.SortDescriptions>
  12:                 <ed:SortDescription PropertyName="PublishDate" Direction="Ascending" />
  13:             </CollectionViewSource.SortDescriptions>
  14:         </ed:CollectionViewSource>
  15:         <e:StringToUriConverter x:Name="stringToUri" />
  16:         <e:FormatDateTimeConverter x:Name="dateToString" />
  17:     </UserControl.Resources>
  18:     <Grid x:Name="LayoutRoot" Background="#ffffffff">
  19:         <Grid.RowDefinitions>
  20:             <RowDefinition Height="*" />
  21:             <RowDefinition Height="80" />
  22:         </Grid.RowDefinitions>
  23:         <Grid.Resources>
  24:             <DataTemplate x:Name="rssTemplate">
  25:                 <Border Background="#ffeeeeee" Margin="2,2,2,0" Padding="5" BorderBrush="#00000000" BorderThickness="1" CornerRadius="3,3">
  26:                     <Grid>
  27:                         <Grid.ColumnDefinitions>
  28:                             <ColumnDefinition Width="50*"/>
  29:                             <ColumnDefinition Width="50*"/>
  30:                         </Grid.ColumnDefinitions>
  31:                         <Grid.RowDefinitions>
  32:                             <RowDefinition Height="60*" />
  33:                             <RowDefinition Height="40*" />
  34:                         </Grid.RowDefinitions>
  35:                         <HyperlinkButton x:Name="itemLabel" Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2" Margin="3,3,3,3" 
  36:                                          Content="{Binding Title.Text}" NavigateUri="{Binding Id, Converter={StaticResource stringToUri}}" 
  37:                                          TargetName="_blank" FontSize="14" FontFamily="Verdana" Foreground="#ffff9900" />
  38:                         <TextBlock Grid.Column="0" Grid.Row="1" Margin="3,3,3,3" FontSize="14" FontFamily="Verdana" 
  39:                                    Foreground="#ff666666" Text="{Binding PublishDate, Converter={StaticResource dateToString}, ConverterParameter=dd.MM.yyyy - hh:mm}" />
  40:                         <TextBlock Grid.Column="1" Grid.Row="1" Margin="3,3,3,3" FontSize="14" FontFamily="Verdana"
  41:                                    Foreground="#ff666666" Text="{Binding Copyright}" />
  42:                     </Grid>
  43:                 </Border>
  44:             </DataTemplate>
  45:         </Grid.Resources>
  46:         <Border Grid.Column="0" Grid.Row="0" Margin="2" BorderBrush="#ffff9900" BorderThickness="1" CornerRadius="3,3">
  47:             <ScrollViewer DataContext="{StaticResource dataSource}" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
  48:                 <ItemsControl ItemsSource="{Binding Source={StaticResource rssView}}" ItemTemplate="{StaticResource rssTemplate}">
  49:                 </ItemsControl>
  50:             </ScrollViewer>
  51:         </Border>
  52:         <Grid Grid.Column="0" Grid.Row="1" Background="#55ff9900" Margin="2">
  53:             <Grid.ColumnDefinitions>
  54:                 <ColumnDefinition Width="*"/>
  55:                 <ColumnDefinition Width="*"/>
  56:             </Grid.ColumnDefinitions>
  57:             <Grid.RowDefinitions>
  58:                 <RowDefinition Height="50*" />
  59:                 <RowDefinition Height="50*" />
  60:             </Grid.RowDefinitions>
  61:             <RadioButton x:Name="sortDescending" TextAlignment="Right" HorizontalAlignment="Left" Margin="10" Grid.Column="1" Grid.Row="1" Grid.ColumnSpan="1" Content="Descending" GroupName="sort" Checked="sort_Checked" IsChecked="true" />
  62:             <RadioButton x:Name="sortAscending" TextAlignment="Left" HorizontalAlignment="Right" Margin="10" Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="1" Content="Ascending" GroupName="sort" Checked="sort_Checked" />
  63:             <WatermarkedTextBox x:Name="txtKey" Padding="3" Grid.Column="0" Grid.Row="0" Margin="5" FontSize="14" Grid.ColumnSpan="2" TextChanged="txtKey_TextChanged" Watermark="Insert Keywork..." />
  64:         </Grid>
  65:     </Grid>
  66: </UserControl>

In this code snippet it is visible how to bind correctly the datasource to the CollectionViewSource and the CollectionViewSource itselft to the consuming ItemsControl. When we have established this connection we hook up the filtering event where we use a Contains() to check that the element contains the text written in the textbox. Every time we press a key the TextChanged event simply call the Refresh() method of the CollectionViewSource causing the filtering to happen again with the new text entered. This is a very intensive use of the  CollectionViewSource but with a few collection items (up to one or two hundred) there will be no side effects. In a production application this scenario probably will be handled in a different way, but I'm sure there is many other times you may take advantage of my control. The sorting happen in a similar way. When a radio button has been hit we swap the direction of the configured sorting and then recall the Refresh() method to rebind the interface. Try changing the SortDescription properties to sort by another property.

   1: public partial class Page : UserControl
   2: {
   3:     public Page()
   4:     {
   5:         InitializeComponent();
   6:     }
   7:  
   8:     private void UserControl_Loaded(object sender, RoutedEventArgs e)
   9:     {
  10:         this.dataSource = this.Resources["dataSource"] as DataSource;
  11:         this.dataSource.Load();
  12:     }
  13:  
  14:     private void sort_Checked(object sender, RoutedEventArgs e)
  15:     {
  16:         CollectionViewSource rssView = this.Resources["rssView"] as CollectionViewSource;
  17:  
  18:         if (rssView != null)
  19:         {
  20:             SortDescription sort = rssView.SortDescriptions[0];
  21:             sort.Direction = (sender == sortAscending ? ListSortDirection.Ascending : ListSortDirection.Descending);
  22:             rssView.Refresh();
  23:         }
  24:     }
  25:  
  26:     private void txtKey_TextChanged(object sender, TextChangedEventArgs e)
  27:     {
  28:         rssView.Refresh();
  29:     }
  30:  
  31:     private void rssView_Filter(object sender, Elite.Silverlight.FilterCollectionEventArgs e)
  32:     {
  33:         SyndicationItem item = e.Item as SyndicationItem;
  34:  
  35:         if (item != null)
  36:         {
  37:             e.Accept = item.Title.Text.ToLower().Contains(txtKey.Text.ToLower()) || txtKey.Text == txtKey.Watermark || txtKey.Text == string.Empty;
  38:         }
  39:     }
  40: }

Now we may run the application and try to enter some text in the TextBox. We will see that the displayed item will change according to the text entered. Also checking and unchecking the sort direction radio button will change the order of the the items.

Conclusion

The CollectionViewSource control is part of my Elite.Silverlight Library 1.0.3037.0 where I'm collecting all my reusable controls and classes. Please download it from codeplex (http://www.codeplex.com/silverlightlibrary) and use for free in your projects. The project come under the CCPL license and is free for non-commercial purposes. For use in commercial project please contact me on my weblog contact form at (http://blog.boschin.it/contact.aspx).

 


Una delle possibilità mancanti in silverlight 2.0 è quella di creare dei controlli con un rendering custom. L'unica soluzione per creare delle forme complesse è quella di comporre tra loro figure più semplici quali poligoni o path. In questo breve esempio voglio mostrarvi come fare a creare un controllo che disegni un poligono regolare di cui sia conosciuto il numero di vertici. Inoltre questo poligono sarà in grado di assumere una percentuale di "stellatura" che indichi la profondità dell'effetto stella ad esso applicato.

star

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


Il primo passo per arrivare al risultato è quello di determinare come costruire la figura. Nel caso del poligono regolare possiamo indifferentemente usare un Polygon o un Path, ma è sicuramente più proficuo e immediato usare un Polygon. Il nostro algoritmo di disegno quindi dovrà determinare la posizione dei vertici rispetto ad un ipotetico centro e tracciare le rette che li congiungono. Considerato che un poligono potrà avere una Width e una Height il processo migliore per determinare i vertici è quello di percorrere un'ellisse inscritta nel rettangolo determinato dall'area e ad intervalli regolari tracciare un segmento che unisca il punto corrente al precedente. In questo modo avremo sempre un poligono che riempia del tutto l'area e che quindi potrà essere anche distorto. Il poligono regolare lo avremo quando Width e Height sono uguali. Ecco il codice necessario all'operazione:

   1: private void DrawPolygon(Size size)
   2: {
   3:     this.Polygon.Width = size.Width;
   4:     this.Polygon.Height = size.Height;
   5:  
   6:     double halfWidth = this.Width / 2;
   7:     double halfHeight = this.Height / 2;
   8:  
   9:     PointCollection points = new PointCollection();
  10:  
  11:     for (double angle = 0; angle < Math.PI * 2; angle += Math.PI * 2 / this.VertexCount)
  12:     {
  13:         double x = halfWidth * Math.Sin(angle) + halfWidth;
  14:         double y = halfHeight * Math.Cos(angle) + halfHeight;
  15:         points.Add(new Point(x, y));
  16:     }
  17:  
  18:     this.Polygon.Points = points;
  19: }

Per ottenere l'effetto di stellatura invece dovremmo suddividere l'elisse in un numero doppio di vertici e tracciarne metà su di essa e metà su una elisse concentrica più interna la cui dimensione sia una frazione (espressa in percentuale) rispetto alla più esterna. Regolando la percentuale di stellatura avremo stelle con punte più o meno acuminate. Ad esempio per ottenere la classica stella a cinque punte dovremmo impostare VertexCount = 5 e StarPercentage = 60%.

   1: private void DrawStar(Size size)
   2: {
   3:     this.Polygon.Width = size.Width;
   4:     this.Polygon.Height = size.Height;
   5:  
   6:     double halfWidth = this.Width / 2;
   7:     double halfHeight = this.Height / 2;
   8:     double xStar = halfWidth / 100 * (100 - this.StarPercentage);
   9:     double yStar = halfHeight / 100 * (100 - this.StarPercentage);
  10:  
  11:     PointCollection points = new PointCollection();
  12:  
  13:     int count = 0;
  14:  
  15:     for (double angle = 0; angle < Math.PI * 2; angle += Math.PI * 2 / (this.VertexCount * 2))
  16:     {
  17:         if (count++ % 2 == 0)
  18:         {
  19:             double x = halfWidth * Math.Sin(angle) + halfWidth;
  20:             double y = halfHeight * Math.Cos(angle) + halfHeight;
  21:             points.Add(new Point(x, y));
  22:         }
  23:         else
  24:         {
  25:             double x = xStar * Math.Sin(angle) + halfWidth;
  26:             double y = yStar * Math.Cos(angle) + halfHeight;
  27:             points.Add(new Point(x, y));
  28:         }
  29:     }
  30:  
  31:     this.Polygon.Points = points;
  32: }

I due metodi riportati sono in realtà alternativi. Nel caso in cui la percentuale di stellatura sia 0 useremo il primo mentre con valori diversi useremo il secondo. Essi operano su una Shape di tipo Polygon inserita in un Canvas da cui eredita il nostro controllo. All'interno del metodo ArrangeOverride provvederemo ad invocare il metodo di disegno.

   1: public class RegularPolygon : Canvas
   2: {
   3:     // .................
   4:  
   5:     protected override Size ArrangeOverride(Size finalSize)
   6:     {
   7:         DrawShape(finalSize);
   8:         return base.ArrangeOverride(finalSize);
   9:     }
  10:     
  11:     // .................
  12: }

A questo punto il controllo potrebbe essere considerato finito se non fosse per il fatto che le proprietà esposte (VertexCount e StarPercentage) sono dei meri contenitori e non sortiscono alcun effetto se ad esse applichiamo uno Style o il DataBinding. Questo tipo di comportamenti infatti hanno la caratteristica di essere applicabili esclusivamente a delle DependencyProperty. La DependencyProperty è una proprietà, registrata all'interno del sistema delle DP che consente di ottenere notifica del cambiamento del suo valore. Essa supporta pienamente il DataBinding e può anche essere oggetto dell'applicazione di uno style oppure di una Animazione.

Creare una DependencyProperty è tutto sommato semplice. Innanzitutto occorre creare un membro statico la cui funzione è di tenere una sorta di puntatore alla DependencyProperty. Al suo interno definiamo il nome della proprieta e il suo tipo nonchè il delegate che sarà incaricato di gestire la notifica del cambio di valore. Il secondo passo è di creare una proprietà che serva ad esporre il valore della DP verso l'esterno. Ecco un esempio:

   1: public static DependencyProperty VertexCountProperty =
   2:     DependencyProperty.Register("VertexCount", typeof(Int32), typeof(RegularPolygon), new PropertyChangedCallback(Property_Changed));
   3:  
   4: //..........
   5:  
   6: public Int32 VertexCount
   7: {
   8:     get { return (Int32)this.GetValue(RegularPolygon.VertexCountProperty); }
   9:     set { this.SetValue(RegularPolygon.VertexCountProperty, value); }
  10: }

Una proprietà dichiarata in questo modo comincerà a funzionare come si addice ad una vera DependencyProperty. Nel metodo Property_Changed infatti riceveremmo le notifiche del cambio di numero vertici e provvederemo a ridisegnare il poligono. Inoltre visto che siamo raffinati dovremmo provvedere ad esporre una serie di proprietà aggiuntive che ci permettano di applicare delle caratteristiche dinamiche. Ad esempio Stroke, Fill, StrokeLineJoin ecosì via. Non potendo ereditare da Shape siamo costretti a usare il contenimento. Ecco il metodo:

   1: private static void Property_Changed(object sender, DependencyPropertyChangedEventArgs args)
   2: {
   3:     Polygon polygon = ((RegularPolygon)sender).Polygon;
   4:     
   5:     if (args.Property == RegularPolygon.StrokeProperty)
   6:              polygon.SetValue(Polygon.StrokeProperty, args.NewValue);
   7:     else if (args.Property == RegularPolygon.StrokeThicknessProperty)
   8:      polygon.SetValue(Polygon.StrokeThicknessProperty, args.NewValue);
   9:     else if (args.Property == RegularPolygon.StrokeLineJoinProperty)
  10:      polygon.SetValue(Polygon.StrokeLineJoinProperty, args.NewValue);
  11:     else if (args.Property == RegularPolygon.StrokeDashArrayProperty)
  12:      polygon.SetValue(Polygon.StrokeDashArrayProperty, args.NewValue);
  13:     else if (args.Property == RegularPolygon.FillProperty)
  14:      polygon.SetValue(Polygon.FillProperty, args.NewValue);
  15:     else if (args.Property == RegularPolygon.VertexCountProperty)
  16:      ((RegularPolygon)sender).ArrangeOverride(new Size(((RegularPolygon)sender).ActualWidth, ((RegularPolygon)sender).ActualHeight));
  17:     else if (args.Property == RegularPolygon.StarPercentageProperty)
  18:      ((RegularPolygon)sender).ArrangeOverride(new Size(((RegularPolygon)sender).ActualWidth, ((RegularPolygon)sender).ActualHeight));
  19: }

A questo punto possiamo introdurre il nuovo controllo nei nostri lavori molto semplicemente ma per questo vi lascio allo studio del progetto allegato. Nella figura qui riportata si vede il controllo completo. Gli slider consentono di speficare rotadione, stellatura e il numero di vertici.

Download: http://blog.boschin.it/download/Elite.Silverlight..RegularPolygon.zip

Technorati Tag: ,

Nonostante in Silverlight 2.0 sia completamente disponibile un linguaggio di programmazione come C#, resistono ancora alcune casistiche in cui occorre fare ricorso all'uso di Javascript per interagire con la pagina. Per questo esiste la classe HtmlPage che funge da entry point della nostra pagina HTML e che facilmente ci consente di interagire con essa dal codice managed.

Esiste però anche la remota possibilità (non poi così remota in effetti...) che avendo del codice Javascript si renda necessario accedere ad un metodo managed esposto dal plugin. A me ad esempio è capitato di dover intercettare l'uso della rotellina del mouse che Silverlight non consente di controllare. Per questo mi sono scritto il codice Javascript e al verificarsi di un evento chiamo un metodo esposto da uno dei miei UserControl. I passi per raggiungere questo scopo sono essenzialmente due:

A) Registrare il controllo che contiene il metodo da esporre con il metodo RegisterScriptableObject()

   1: public MapControl()
   2: {
   3:     // Required to initialize variables
   4:     InitializeComponent();
   5:  
   6:     HtmlPage.RegisterScriptableObject("MapControl", this);
   7: }

Questo passaggio farà in modo che il plugin esponga all'interno della proprietà content una proprietà denominata "MapControl" che contiene il riferimento al nostro UserControl. Naturalmente possiamo esporre sia un singolo controllo che la pagina stessa.

B) Decorare i membri del controllo che devono essere visibili al Javascript con l'attributo [ScriptableMember]

   1: [ScriptableMember]
   2: public string HandleMouseWheel(double mouseX, double mouseY, int wheelDelta)
   3: {
   4:     //... do your work here
   5: }

A questo punto da javascript sarà molto semplice trovare una referenza al plugin, conoscendo il nome del controllo, e chiamare il metodo esposto:

   1: var plugin = document.getElementById("pluginId");
   2:  
   3: plugin.content.MapControl.HandleMouseWheel(123, 456, 7);

Semplice ed efficace. L'uso di un attributo per marcare le proprietà e i metodi è da tenere in seria considerazione. Infatti in questo modo metteremo al sicuro il nostro codice da chiamate "indiscrete" a metodi che non hanno alcuno scopo valido.


Il fatto di avere a che fare con un runtime che permette di eseguire CLR non significa che non serva mai ricorrere all'uso di Javascript. M'è capitato ormai svariate volte di dovermi agganciare ad un evento del DOM HTML  o di dover in qualche modo interagire con altri oggetti nella pagina. In Silverlight 2.0 è possibile fare ciò per mezzo dell'oggetto HtmlPage che espone un punto di accesso al contenuto della pagina. Con esso possiamo ad esempio collegarci ad eventi del dom (AttachEvent) o modificare elementi e attributi o ancora invocare metodi Javascript.

A me è capitato ad esempio di dover interagire con uno ScriptControl che normalmente viene registrato nella pagina dalla Ajax Library ed è accessibile mediante il metodo $find(). Per invocare il medesimo metodo da Silverlight occorre usare la seguente sintassi:

   1: ScriptObject map = 
   2:     (ScriptObject)HtmlPage.Window.Invoke("$find", "map");

Ci sono casi in cui non è disponibile uno shortcut definito come nel caso di $find. Ad esempio se dobbiamo usare il metodo Sys.UI.DomElement.getBounds() le cose si complicano notevolmente:

   1: ((ScriptObject)((ScriptObject)((ScriptObject)((ScriptObject)
   2:         HtmlPage.Window
   3:             .GetProperty("Sys"))
   4:             .GetProperty("UI"))
   5:             .GetProperty("DomElement"))
   6:             .Invoke("getBounds", HtmlPage.Document.Body));

In sostanza dobbiamo cercare ricorsivamente le proprietà e alla fine invocare il metodo. COn un po' di codice è possibile scrivere dei metodi che aiutino a semplificare questo lavoro:

   1: /// <summary>
   2: /// Invokes the ex.
   3: /// </summary>
   4: /// <param name="so">The so.</param>
   5: /// <param name="methodPath">The method path.</param>
   6: /// <param name="args">The args.</param>
   7: /// <returns></returns>
   8: public static object InvokeEx(this ScriptObject so, string methodPath, params object [] args)
   9: {
  10:     IEnumerable<string> parts = Parse(methodPath);
  11:  
  12:     foreach (string part in parts)
  13:     {
  14:         if (part != parts.Last())
  15:             so = (ScriptObject)so.GetProperty(part);
  16:         else
  17:             return so.Invoke(part, args);
  18:     }
  19:  
  20:     throw new ArgumentException("methodPath");
  21: }
  22:  
  23: /// <summary>
  24: /// Gets the property ex.
  25: /// </summary>
  26: /// <param name="so">The so.</param>
  27: /// <param name="propertyPath">The property path.</param>
  28: /// <returns></returns>
  29: public static ScriptObject GetPropertyEx(this ScriptObject so, string propertyPath)
  30: {
  31:     IEnumerable<string> parts = Parse(propertyPath);
  32:  
  33:     foreach (string part in parts)
  34:         so = (ScriptObject)so.GetProperty(part);
  35:  
  36:     return so;
  37: }
  38:  
  39: /// <summary>
  40: /// Parses the specified path.
  41: /// </summary>
  42: /// <param name="path">The path.</param>
  43: /// <returns></returns>
  44: private static IEnumerable<string> Parse(string path)
  45: {
  46:     Match match = Regex.Match(path, @"(?<part>[^\.]*)(\.(?<part>[^\.]*))*");
  47:  
  48:     if (match.Success)
  52:         return from Capture cp in match.Groups["part"].Captures
  53:                select cp.Value;
  55:  
  56:     throw new ApplicationException(string.Format("Unable to parse path '{0}'}", path));
  57: }

Si tratta di un paio di Extension Method che estendono la classe ScriptObject e consentono di specificare il metodo o la proprietà con la classica notazione puntata. Il metodo principale, Parse, fa uso di una regular expression per verificare la validità della stringa passata ed estrarre la varie porzioni. Ecco come usare il nuovo metodo:

   1: ScriptObject bounds = 
   2:     (ScriptObject)HtmlPage.Window.InvokeEx("Sys.UI.DomElement.getBounds", HtmlPage.Document.Body);
Technorati Tag: ,,

Ho già parlato un po' di tempo fa di con riferimento ad ASP.NET. Stasera voglio tornarci su per parlare della medesima caratteristica di Silverlight 2.0. L'event bubbling è quella particolare caratteristica che fa in modo che un evento sollevato da un elemento di propaghi agli elementi immediatamente superiori e così via fino a raggiungere la radice della gerarchia. Il bubbling può essere molto utile ma per poterlo sfruttare al meglio è necessario essere in grado di interrompere la propagazione dell'evento in un particolare punto.

In Silverlight 1.0 gestire l'event bubbling è problematico perchè non esiste un metodo "istituzionale" per per interrompere la propagazione dell'evento ma per farlo occorre appoggiarsi a delle variabili globali da valorizzare opportunamente. In Silverlight 2.0 invece è stato introdotto un argomento Handled che opportunamente valorizzato e gestito consente di gestire con facilità questo problema.

   1: private void child_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
   2: {
   3:     // handle event
   4:  
   5:     e.Handled = true;
   6: }
   7:  
   8: //........
   9:  
  10: private void father_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
  11: {
  12:     if (!e.Handled)
  13:     {
  14:         // event was not handled
  15:     }
  16: }

Naturalmente questa proprietà si applica agli eventi che subiscono il bubbling. In particolare gli eventi che lo gestiscono sono tutti quelli relativi il mouse.


Uno delle cose che mancano in Silverlight, sin dalla versione 1.0, è la possibilità di gestire l'evento DoubleClick del Mouse. Pur se Silverlight è ricco di eventi che riguardano il mouse è carente secondo due punti di vista. Il tasto destro del mouse non è gestibile, ma questo lo si deve alla presenza di un menù di contesto di configurazione del plugin. Ma l'assenza di doppio click è davvero singolare e talvolta fastidiosa. Perciò, ora che è uscita la beta 1 di Silverlight 2.0, e preso atto che la mancanza è ancora presente ho pensato di creare una classina che mi venga in aiuto quando ho la necessità di distinguere tra singolo e doppio click.

La questione è piuttosto semplice. Abbiamo a disposizione sono l'evento MouseLeftButtonUp e da esso dobbiamo discriminare ed è piuttosto ovvio che la variabile da tenere in considerazione sia il tempo. Potremmo immaginare che quando arriva un primo evento dobbiamo attendere un certo numero di millisecondi (diciamo 300 circa). Se entro questo tempo arriva un secondo evento siamo in presenza di un doppio click mentre al loro scadere possiamo considerare di avere a che fare con un single click.

Gestire questa condizione comporta però un bel po' di lavoro e soprattutto saremmo costretti a scrivere questo flusso svariate volte, almeno una volta per ciascun elemento per il quale abbiamo la necessità di gestire il doppio click. Perciò da buoni programmatori, convinti che il nostro lavoro sia di scrivere poco codice, dobbiamo realizzare una classe che ci aiuti ogni volta che ne abbiamo bisogno.

La classe che ho realizzato e che trovate in allegato a questo post prende in carico tutti gli eventi di click che arrivano da una determinata sorgente e applica la logica cui ho accennato poco fa. Poi. alla scadenza dei 300 millisecondi si incarica di sollevare a sua volta un altro evento corrispondente al singolo o doppio click. Purtroppo, dato che per ottenere l'eventuale secondo click dobbiamo restituire immediatamente il controllo al chiamante, siamo costretti a lanciare un thread che si incarichi di attendere il timeout e lanciare l'evento Click (quello singolo) nel caso in cui non arrivi più nulla.  Ecco il corpo principale del MouseClickManager:

   1: /// <summary>
   2: /// Handles the click.
   3: /// </summary>
   4: /// <param name="sender">The sender.</param>
   5: /// <param name="e">The <see cref="System.Windows.Input.MouseButtonEventArgs"/> instance containing the event data.</param>
   6: public void HandleClick(object sender, MouseButtonEventArgs e)
   7: {
   8:     lock(this)
   9:     {
  10:         if (this.Clicked)
  11:         {
  12:             this.Clicked = false;
  13:             OnDoubleClick(sender, e);
  14:         }
  15:         else
  16:         {
  17:             this.Clicked = true;
  18:             ParameterizedThreadStart threadStart = new ParameterizedThreadStart(ResetThread);
  19:             Thread thread = new Thread(threadStart);
  20:             thread.Start(e);
  21:         }
  22:     }
  23: }
  24:  
  25: /// <summary>
  26: /// Resets the clicked flag after timeout.
  27: /// </summary>
  28: /// <param name="state">The state.</param>
  29: private void ResetThread(object state)
  30: {
  31:     Thread.Sleep(this.Timeout);
  32:  
  33:     lock (this)
  34:     {
  35:         if (this.Clicked)
  36:         {
  37:             this.Clicked = false;
  38:             OnClick(this, (MouseButtonEventArgs)state);
  39:         }
  40:     }
  41: }

Nel metodo HandleClick dapprima acquisiamo un lock. E' una precauzione necessaria perchè come vedremo il flag Clicked diventa una risorsa condivisa fra due thread diversi e quindi il lock ci serve ad evitare problemi di concorrenza su di essa. A questo punto i casi sono due: se troviamo Clicked settato a false significa che siamo in presenza del primo click e quindi dopo aver settato il flag a true avviamo un thread secondario. Se invece Clicked è già settato significa che è arrivato il secondo click perciò si azzera il flag e poi si provvede a sollevare l'evento b. Nel thread secondario innanzitutto si attendono i millisecondi configurati nella proprietà Timeout e al loro scadere se il flag è ancora settato lo si azzera e si solleva l'evento Click. Fermatevi un attimo a pensarci su se la cosa non è così chiara... comunque vi assicuro che funziona.

Ora c'è un problema da gestire. Dato che l'evento Click viene sollevato da un thread secondario quando questo arriva all'interfaccia ci troveremo nella spiacevole condizione di non essere sincronizzati con il thread principale che appunto detiene l'interfaccia stessa. Questo significa che se tutto va bene... non funzionerà nulla :) Abbiamo bisogno di fare il marshaling del contesto per rendere l'evento Click fruibile dall'interfaccia. Questo è un problema comune all'ambiente Windows Forms che si ripresenta anche all'interno del plugin di Silverlight. Ecco il corpo del metodo OnClick deputato a sollevare l'evento:

   1: /// <summary>
   2: /// Called when click occure.
   3: /// </summary>
   4: /// <param name="sender">The sender.</param>
   5: /// <param name="e">The <see cref="System.Windows.Input.MouseButtonEventArgs"/> instance containing the event data.</param>
   6: private void OnClick(object sender, MouseButtonEventArgs e)
   7: {
   8:     MouseButtonEventHandler handler = Click;
   9:  
  10:     if (handler != null)
  11:         this.Control.Dispatcher.BeginInvoke(handler, sender, e);
  12: }

L'oggetto rappresentato dalla proprietà Control è l'istanza del controllo che dovrà ricevere l'evento. Questo deve essere passato al costruttore della classe assieme al timeout desiderato. Il dispatcher si occuperà per noi di gestire il marshaling del contesto e il gioco è fatto.

Usare la classe MouseClickManager è semplice. Se ne crea un'istanza passandole un riferimento al controllo che la contiene e un timeout. Poi si agganciano i due eventi DoubleClick e Click e infine si fa in modo che all'interno dell'evento MouseLeftButtonUp venga chiamato il metodo HandleClick e si passano ad esso gli argomenti sender e args.

La scelta del timeout è soggettiva. Sarebbe opportuno poter configurare la soglia in base all'abilità dell'utente. Comunque da qualche esperimento che ho fatto si evince che 200 millisecondi sono troppo pochi mentre 400 sono troppi e si inizia ad apprezzare il ritardo dell'evento Click rispetto al clieck vero e proprio.

Download: http://blog.boschin.it/download/mouseclickmanager.zip (1 KB)

 

Technorati Tag: ,

Visto che ci riferiamo ad un plugin del browser, parlare di thread e di marshaling può sembrare una pura utopia. Invece se provate a guardare le classi esposte da Silverlight 2.0 noterete che il namespace System.Threading è implementato quasi al 100%. Thread, Timer, ThreadPool. Tutto e molto di più è a nostra disposizione per lanciare dei thread che si occupino di fare del lavoro in parallelo. Questo però apre la strada ad un problema classico, che tipicamente si incontra nelle Windows Forms. Se ad esempio lanciate un thread in una Form, e questo thread tenta di aggiornare una parte dell'interfaccia (ad esempio scrivendo in una TextBox) ci troveremmo in presenza di un problema dovuto al fatto che il thread secondario e l'interfaccia sono in due thread separati e ogni accesso che tenti di superare la barriera che li separa è destinato a fallire in modo casuale. In effetti il risultato dui questa operazione è indeterminato tanto che il più delle volte fallirà ma talvolta potrebbe anche andare a buon fine.

Per risolvere questo problema, che in Silverlight è analogo è necessario usare l'oggetto Dispatcher. Questo oggetto è esposto dalla classe Control e di conseguenza da qualunque controllo di Silverlight. Il Dispatcher di Silverlight espone il metodo BeginInvoke() che è in grado di chiamare un delegate effettuando quello che va sotto il nome di Marshaling. In buona sostanza quello che avviene è che viene immesso un messaggio nella coda del thread principale il quale alla ricezione si occuperà di chiamare il delegate. Ecco come fare:

   1: /// <summary>
   2: /// Called when click occure.
   3: /// </summary>
   4: /// <param name="sender">The sender.</param>
   5: /// <param name="e">The <see cref="System.Windows.Input.MouseButtonEventArgs"/> instance containing the event data.</param>
   6: private void OnClick(object sender, MouseButtonEventArgs e)
   7: {
   8:     MouseButtonEventHandler handler = Click;
   9:  
  10:     if (handler != null)
  11:         this.Control.Dispatcher.BeginInvoke(handler, sender, e);
  12: }

Il metodo BeginInvoke() richiede in ingresso il delegate da chiamare e qualora di tratti di un evento consente di specificare gli argomenti che esso dovrà ricevere. Si tratta di una param list perciò il delegate o il callback potrà ricevere un numero indefinito di parametri. Il controllo da usare per trovare una referenza al Dispatcher è il destinatario dell'evento che dobbiamo sollevare. Un buon candidato potrebbe essere lo UserControl.


Anche Silverlight 2.0 come la versione precedente ha la possibilità di attivare la modalità Windowsless cioè quella particolare modalità per cui il plugin diventa trasparente laddove non vi siano aree opache. A prescindere dal fatto che l'uso di questa modalità va ben ponderato perchè la sua attivazione comporta un onere molto maggiore in termini di elaborazione, i risultati che si possono ottenere sono davvero molto "impressive" e le applicazioni si sprecano...

Ho trovato qualche difficicoltà a riprodurre il medesimo comportamento, tuttavia ho scoperto che l'unico problema è quello di trovare la giusta "taratura" delle proprietà. Ecco come impostare il plugin con il nuovo controllo ASP.NET:

   1: <asp:Silverlight ID="overlay" runat="server" 
   2:                  Source="~/ClientBin/SilverlightApplication1.xap" 
   3:                  Windowless="true" 
   4:                  PluginBackground="#00000000" 
   5:                  Version="2.0" 
   6:                  Width="100%" Height="100%"
   7:                  style="z-index:200;" />

Il "segreto" è quello di impostare il colore del background del plugin a trasparente usando un alpha channel con la proprietà PluginBackground. Diversamente anche se la proprietà Windowless è impostata a True, il colore opaco dello sfondo del plugin renderà vano ogni tentativo. Il colore #00000000 in prima analisi potrebbe apparire come un nero, ma i primi due zeri che sono in più rispetto alla normale sintassi HTML corrispondono al grado di opacità e 00 ovviamente significa "trasparente". In più impostare uno z-index serve a "sollevare" il plugin e portaro in primo piano rispetto al resto. A voi eventualmente la decisione di applicare stili per il posizionamento assoluto qualora abbiate la necessità di sovrapporlo a delle parti di HTML.

- English version (automatic translation)


La domanda non è oziosa e la risposta non è "nulla" ma nemmeno "tutto". La licenza che accompagna Silverlight 2.0 è di tipo Go-Live e questo significa che è possibile pubblicare i propri lavori ma solo entro certi limiti. I limiti riportati nel documento di licenza sono i seguenti:

  • you are fully responsible for any and all damages that may result due to any failure of the software;
  • you must notify all recipients that your Silverlight applications rely on pre-release, time-limited, unsupported software that may not operate correctly;
  • you may not make any representation, warranty or promise on behalf of Microsoft or with respect to the software, or its performance;
  • you may not make available any Silverlight applications that conduct e-commerce transactions, that collect personally identifiable information or confidential data, or which are for hazardous environments that require fail safe controls; and
  • you may not offer Silverlight applications on a commercial basis, such as under a paid subscription or on ad-funded web pages.

Quindi se fate qualche eseperimento interessante potete tranquillamente pubblicarlo, a patto che rispettiate i punti elencati. E' comunque escluso il lucro... almeno è per ora.