-----------------------------------

Acquista i software ArcGIS tramite Studio A&T srl, rivenditore autorizzato dei prodotti Esri.

I migliori software GIS, il miglior supporto tecnico!

I migliori software GIS, il miglior supporto tecnico!
Azienda operante nel settore GIS dal 2001, specializzata nell’utilizzo della tecnologia ArcGIS e aderente ai programmi Esri Italia Business Network ed Esri Partner Network

-----------------------------------



Visualizzazione post con etichetta Json. Mostra tutti i post
Visualizzazione post con etichetta Json. Mostra tutti i post

domenica 29 aprile 2012

Json & ArcObjects

Con ArcGIS 10 sono state introdotte nuove classi che permettono di serializzare e deserializzare in JSON.
Stiamo parlando delle classi JSONSerializerGdb e JSONDeserializerGdb  che permettono di serializzare e deserializzare oggetti di alto livello in classi JSON e da JSON.

Ad esempio, se dovessimo deserializzare una geometria JSON ESRI (vedi GeoService REST Specification ) in ArcObjects, possiamo scrivere:


string jsonGeometryPoint = "{\"x\" : -118.15, \"y\" : 33.80, \"spatialReference\" : {\"wkid\" : 4326}}";
            IJSONReader jsonReader = new JSONReaderClass();
            jsonReader.ReadFromString(jsonGeometryPoint);
            IJSONDeserializer jsonDeserializer = new JSONDeserializerGdbClass();
            jsonDeserializer.InitDeserializer(jsonReader, null);
            IGeometry geometry = ((IExternalDeserializerGdb)jsonDeserializer).ReadGeometry(esriGeometryType.esriGeometryPoint);
            IPoint point = (IPoint)geometry;
            Console.Write("X:{0} - Y:{1} - Factory Code: {2}", point.X, point.Y, point.SpatialReference.FactoryCode);
            Console.Read();


Se dovessimo deserializzare uno spatial Reference JSON ESRI in ArcObjects, possiamo operare in modo analogo:


string jsonSpatialReference = "{\"wkid\" : 4326}";
            IJSONReader jsonReader = new JSONReaderClass();
            jsonReader.ReadFromString(jsonSpatialReference);
            IJSONDeserializer jsonDeserializer = new JSONDeserializerGdbClass();
            jsonDeserializer.InitDeserializer(jsonReader, null);
            ISpatialReference spatialReference = ((IExternalDeserializerGdb)jsonDeserializer).ReadSpatialReference();
            Console.Write("Name:{0} - FactoryCode:{1}", spatialReference.Name, spatialReference.FactoryCode);
            Console.Read();


Ora vediamo i casi inversi, ovverosia serializzare in classi JSON geometrie e spatialReference ArcObjects:


            ////ArcObjects Geometry to json geometry ESRI
            IPoint point = new PointClass();
            point.PutCoords(-118.15, 33.80);
            IJSONWriter jsonWriter = new JSONWriterClass();
            jsonWriter.WriteToString();
            IJSONSerializer jsonSerializer = new JSONSerializerGdbClass();
            jsonSerializer.InitSerializer(jsonWriter, null);
            ((IExternalSerializerGdb)jsonSerializer).WriteGeometry(null, point);
            Console.Write(Encoding.UTF8.GetString(jsonWriter.GetStringBuffer()));
            Console.Read();
 
 
            ////ArcObjects spatialReference to json spatialReference ESRI
            Type factoryType = Type.GetTypeFromProgID("esriGeometry.SpatialReferenceEnvironment");
            object obj = Activator.CreateInstance(factoryType);
            ISpatialReferenceFactory spatialReferenceFactory = obj as ISpatialReferenceFactory;
            IProjectedCoordinateSystem spatialReference = spatialReferenceFactory.CreateProjectedCoordinateSystem((int)esriSRProjCSType.esriSRProjCS_World_Mercator);
            IJSONWriter jsonWriter = new JSONWriterClass();
            jsonWriter.WriteToString();
            IJSONSerializer jsonSerializer = new JSONSerializerGdbClass();
            jsonSerializer.InitSerializer(jsonWriter, null);
            ((IExternalSerializerGdb)jsonSerializer).WriteSpatialReference(null, spatialReference);
            Console.Write(Encoding.UTF8.GetString(jsonWriter.GetStringBuffer()));
            Console.Read();

oppure
IJSONConverterGeometry jsonConvert = new JSONConverterGeometryClass();
            IJSONObject spatRefObj = new JSONObjectClass();
            jsonConvert.QueryJSONSpatialReference(spatialReference, spatRefObj);
            JsonObject spatRefJson = new JsonObject(spatRefObj.ToJSONString(null));

Per convertire una feature class possiamo impostare un Recordset e, nel caso, filtrarlo sia come campi che come record tramite un QueryFilter (2° argomento del metodo SetSourceTable di RecordSet)
            //convert feature class in json esri
            Type factoryType = Type.GetTypeFromProgID("esriDataSourcesGDB.FileGDBWorkspaceFactory");
            IWorkspaceFactory workspaceFactory = (IWorkspaceFactory)Activator.CreateInstance(factoryType);
            IWorkspace workspace = workspaceFactory.OpenFromFile(@"E:\Temp\Test2.gdb", 0);
            IFeatureWorkspace featureWorkspace = (IFeatureWorkspace)workspace;
            IFeatureClass featureClass = featureWorkspace.OpenFeatureClass("streets");
 
            IRecordSet recordset = new RecordSet();
            IRecordSetInit recordSetInit = recordset as IRecordSetInit;
            recordSetInit.SetSourceTable(featureClass as ITablenull);
 
            IGeometryResultOptions geometryResultOptions = new GeometryResultOptionsClass();
            geometryResultOptions.GeneralizeGeometries = true;
            geometryResultOptions.DensifyGeometries = true;
            IPropertySet propertySet = new PropertySetClass();
            propertySet.SetProperty("GeometryResultOptions", geometryResultOptions);
            IJSONWriter jsonWriter = new JSONWriterClass();
            jsonWriter.WriteToString();
            IJSONSerializer jsonSerializer = new JSONSerializerGdbClass();
            jsonSerializer.InitSerializer(jsonWriter, null);
            jsonSerializer.WriteObject(recordset, propertySet);
            Console.Write(Encoding.UTF8.GetString(jsonWriter.GetStringBuffer()));
            Console.Read();


Da json features a feature class:

            string jsonFeatures = "{\"displayFieldName\":\"\",\"fieldAliases\":{\"OBJECTID\":\"OBJECTID\",\"Indirizzo\":\"Indirizzo\"},\"geometryType\":\"esriGeometryPoint\",\"spatialReference\":{\"wkid\":102100},\"fields\":[{\"name\":\"OBJECTID\",\"type\":\"esriFieldTypeOID\",\"alias\":\"OBJECTID\"},{\"name\":\"Indirizzo\",\"type\":\"esriFieldTypeString\",\"alias\":\"Indirizzo\",\"length\":50}],\"features\":[" +
            "{" +
             "\"geometry\": {" +
                "\"x\": 940411.3699657875," +
                "\"y\": 5643498.120243863," +
                "\"spatialReference\": {\"wkid\": 102100}" +
              "}," +
              "\"attributes\": {" +
                "\"OBJECTID\": 1," +
                "\"Indirizzo\": \"VIA SORDI, 1\"" +
              "}" +
            "}]}";
 
            IJSONReader jsonReader = new JSONReaderClass();
            jsonReader.ReadFromString(jsonFeatures);
 
            IJSONConverterGdb JSONConverterGdb = new JSONConverterGdbClass();
            IPropertySet originalToNewFieldMap;
            IRecordSet recorset;
            JSONConverterGdb.ReadRecordSet(jsonReader, nullnullout recorset, out originalToNewFieldMap);
 
            Type factoryType = Type.GetTypeFromProgID("esriDataSourcesGDB.FileGDBWorkspaceFactory");
            IWorkspaceFactory workspaceFactory = (IWorkspaceFactory)Activator.CreateInstance(factoryType);
            IWorkspace workspace = workspaceFactory.OpenFromFile(@"C:\Temp\testsd\sd.gdb", 0);
            IRecordSet2 recordSet2 = recorset as IRecordSet2;
            recordSet2.SaveAsTable(workspace, "Test");
 
Ricordarsi di utilizzare l'Activator per oggetti singleton (SpatialReferenceEnvironmental e FileGDBWorkspaceFactory).

Gli shortcut a questi metodi li potete trovare anche nell'assembly ESRI.ArcGIS.SOESupport nella classe Conversion con i metodi ToGeometry, ToJson e ToSpatialReference. Sempre in questo assembly potete utilizzare i metodi della classe JsonObject per serializzare e deserializzare:

Esempio di deserializzazione
            JsonObject jsonPoint;
            if (!operationInput.TryGetJsonObject("location"out jsonPoint))
                throw new ArgumentNullException("location");
            IPoint location = Conversion.ToGeometry(jsonPoint,
                                                    esriGeometryType.esriGeometryPoint) as
                                                    IPoint;

Esempio di serializzazione
List<JsonObject> jsonGeometries = new List<JsonObject>();
            JsonObject jsonResultsGeometry = Conversion.ToJsonObject(resultsGeometry);
            jsonGeometries.Add(jsonResultsGeometry);
            JsonObject resultJsonObject = new JsonObject();
            resultJsonObject.AddArray("geometries", jsonGeometries.ToArray());
            byte[] result = Encoding.UTF8.GetBytes(resultJsonObject.ToJson());
            return result;


Con il MapServer possiamo utilizzare i metodi dell'interfaccia IMapServer3 che ci consentono agevolmente di avere dei risultati in JSON (mediante l'interfaccia IQueryResult) o Recordset da convertire in JSON da query che filtrano e riproiettano anche con datum differente (vedi IQueryResultsOptions)
IQueryResultOptions queryResultOptions = new QueryResultOptions();
            queryResultOptions.Format = esriQueryResultFormat.esriQueryResultRecordSetAsObject;
            IMapServer3 mapServer = serverObjectHelper.ServerObject as IMapServer3;
            IRecordSet rs = MapServer.QueryFeatureData2(mapServer.DefaultMapName, aLayerDescription, aspatialFilterOut, queryResultOptions).Object as IRecordSet;
            return Conversion.ToJson(rs);


IQueryResultOptions resultOptions = new QueryResultOptionsClass();
            resultOptions.Format = esriQueryResultFormat.esriQueryResultJsonAsMime;
            IMapTableDescription tableDesc = GetTableDesc(mapServer, layerID);
            IQueryResult result = mapServer.QueryData(mapServer.DefaultMapName, tableDesc, filter, resultOptions);
            return result.MimeData;

mercoledì 30 novembre 2011

Deserializzazione JSON: polimorfismo

Quando si ha a che fare con il formato JSON, ad esempio quando richiamiamo i servizi rest SOE ESRI, .NET ci mette a disposizione DataContractJsonSerializer che ci consente di serializzare direttamente oggetti di tipo .NET in dati JSON e di deserializzare tali dati in istanze di tipi .NET. Tuttavia, uno dei problemi sorge quando il servizio REST ci restituisce istanze di oggetti diversi o derivati. Javascript è un linguaggio con tipizzazione debole e l'identità del tipo non rappresenta solitamente un problema. Il problema si presenta quando si utilizza JSON per le comunicazioni tra un sistema fortemente tipizzato (.NET) e uno con tipizzazione debole (Javascript): è utile allora mantenere l'identità del tipo.

Prendiamo questo esempio: i tipi con nomi di contratto dati "Envelope" e "Point" derivano da un tipo con nome di contratto dati "Geometry". Se "Point" viene inviato da .NET a Javascript e viene quindi restituito ad un metodo .NET che prevede "Geometry", è utile per .NET sapere che l'oggetto in questione è stato originariamente un "Point"; in caso contrario, tutte le informazioni specifiche del tipo derivato (ad esempio i membri dati "x" e "y" su "Point") potrebbero andare perdute.
Per mantenere l'identità del tipo, durante la serializzazione di tipi complessi in JSON, può essere aggiunto un suggerimento; il deserializzatore riconosce tale suggerimento e agisce di conseguenza. Il suggerimento relativo ai tipi è rappresentato da una coppia chiave/valore JSON dove il nome della chiave è "__type" (due caratteri di sottolineatura seguiti dalla parola "type"). Il valore è rappresentato invece da una stringa JSON con forma 'DataContractName:DataContractNamespace' (tutto il testo che precede i due punti rappresenta il nome).

Per ridurre le dimensioni dei messaggi JSON, il prefisso dello spazio dei nomi del contratto dati predefinito viene sostituito dal carattere "#". Per rendere reversibile questa sostituzione, viene utilizzata una regola di escape: se il primo carattere nello spazio dei nomi è "#" o "\", verrà aggiunto un carattere "\"). Pertanto, se "Point" è un tipo nello spazio dei nomi .NET "Studioat.Geometry", il relativo spazio dei nomi del contratto dati sarà: http://schemas.datacontract.org/2004/07/Studioat.Geometry. Le forme e la rappresentazione JSON appaiono nel modo seguente.

//prima forma
string a1 = "{\"__type\":\"Point:http:\\/\\/schemas.datacontract.org\\/2004\\/07\\/Studioat.Geometry\",\"X\":10.256,\"Y\":80.729}";
//seconda forma
string a2 = "{\"__type\":\"Point:#Studioat.Geometry\",\"X\":10.256,\"Y\":80.729}";

Sia i nomi troncati (#Studioat.Geometry) che quelli completi vengono riconosciuti durante la deserializzazione. Inoltre il suggerimento relativo ai tipi deve essere visualizzato all'inizio nella rappresentazione JSON. Questo è il solo caso in cui l'ordine delle coppie chiave/valore è importante nell'elaborazione di JSON.
Di seguito viene riportato un esempio di modalità non valida per specificare un suggerimento relativo ai tipi.
string a2 = "{\"X\":10.256,\"Y\":80.729,\"__type\":\"Point:#Studioat.Geometry\"}";



using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
 
namespace Studioat.ScratchCode
{
 class Program
 {
  static void Main(string[] args)
  {
     string a1 = "{\"__type\":\"Point:http:\\/\\/schemas.datacontract.org\\/2004\\/07\\/Studioat.Geometry\",\"X\":10.256,\"Y\":80.729}";
     string a2 = "{\"__type\":\"Point:#Studioat.Geometry\",\"X\":10.256,\"Y\":80.729}";
 
     string b1 = "{\"__type\":\"Envelope:http:\\/\\/schemas.datacontract.org\\/2004\\/07\\/Studioat.Geometry\",\"XMin\":10.256,\"YMin\":80.729,\"XMax\":21.956,\"YMax\":34.712}";
     string b2 = "{\"__type\":\"Envelope:#Studioat.Geometry\",\"XMin\":10.256,\"YMin\":80.729,\"XMax\":21.956,\"YMax\":34.712}";
 
     byte[] rawstring = System.Text.Encoding.Unicode.GetBytes(a1);
     MemoryStream stream = new MemoryStream(rawstring);
 
     DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(Geometry.Geometry), new Type[] { typeof(Geometry.Envelope), typeof(Geometry.Point) });
 
     Geometry.Geometry o = serializer.ReadObject(stream) as Geometry.Geometry;
     if (o is Geometry.Point)
     {
      Geometry.Point j = o as Geometry.Point;
      Console.WriteLine("Point x:{0} y:{1}", j.X, j.Y);
     }
     else if (o is Geometry.Envelope)
     {
      Geometry.Envelope j = o as Geometry.Envelope;
      Console.WriteLine("Envelope xmin:{0} ymin:{1} xmax:{2} ymax:{3}", j.XMin, j.YMin, j.XMax, j.YMax);
     }
     Console.Read();
  }
 }
}
 
namespace Studioat.Geometry
{
 [DataContract]
 public abstract class Geometry : ICloneable
 {
    
  protected Geometry()
  {
  }
 
  public abstract object Clone();
 }
 
 [DataContract]
 public class Point : Geometry
 {
 
  private double x;
  private double y;
 
 
  public Point()
  {
   this.x = double.NaN;
   this.y = double.NaN;
 
  }
 
  public Point(double x, double y)
  {
   this.x = x;
   this.y = y;
  }
 
  public override object Clone()
  {
   Studioat.Geometry.Point point = base.MemberwiseClone() as Studioat.Geometry.Point;
   return point;
  }
 
  [DataMember]
  public double X
  {
   get;
   set;
  }
 
  [DataMember]
  public double Y
  {
   get;
   set;
  }
 }
 
 [DataContract]
 public class Envelope : Geometry
 {
 
  private double maxx;
  private double maxy;
  private double minx;
  private double miny;
 
  public Envelope()
   : this(double.NaN, double.NaN, double.NaN, double.NaN)
  {
  }
 
  public Envelope(Point minPoint, Point maxPoint)
   : this(minPoint.X, minPoint.Y, maxPoint.X, maxPoint.Y)
  {   
  }
 
  public Envelope(double minx, double miny, double maxx, double maxy)
  {
   this.minx = minx;
   this.miny = miny;
   this.maxx = maxx;
   this.maxy = maxy;
  }
 
  public override object Clone()
  {
   Envelope envelope = base.MemberwiseClone() as Envelope;
   return envelope;
  }
 
  [DataMember]
  public double XMax
  {
   get;
   set;
  }
 
  [DataMember]
  public double XMin
  {
   get;
   set;
  }
 
  [DataMember]
  public double YMax
  {
   get;
   set;
  }
 
  [DataMember]
  public double YMin
  {
   get;
   set;
  }
 }
}

lunedì 25 maggio 2009

Parse Json ArcGISOnline per Silverlight

In .NET Silverlight 2.0 abbiamo la classe WebClient che fornisce la più semplice classe per la comunicazione a servizi.
Per utilizzarla, si deve fornire il contenuto dell'Uri al quale si vuole accedere e poi chiamare il metodo DownloadStringAsync o il metodo OpenReadAsync per scaricare i dati.
Nel caso che vediamo, stiamo usando OpenReadAsync. Utilizzando questi metodi dobbiamo impostare una funzione di callback per prendere i dati restituiti. Quando il trasferimento dei dati è terminato, viene chiamata la nostra funzione client_OpenReadCompleted che riceve un oggetto (argomento: OpenReadCompletedEventArgs) che contiene una proprietà di nome Result che contiene l'oggetto restituito.
In questo esempio, facciamo una chiamata al locator EU degli ArcGIS Online con una risposta di tipo JSON.
Esempio di risposta:
{
"address" :
{
"Address" : "VIA LUCIANO MANARA 48",
"City" : "MONZA",
"Postcode" : "20052",
"Country" : "",
"Loc_name" : "ITASMRVATGeoco"
},
"location" :
{
"x" : 9.25588354543629,
"y" : 45.5901805291887,
"spatialReference" : {
"wkid" : 4326
}
}
}


Nello XAML utilizzeremo una nostra classe (JSONFormatter) che implementa IvalueConverter per il binding.

 
public partial class Page : UserControl
{
JsonObject results;
WebClient client;
public Page()
{
InitializeComponent();
// Send HTTP request to the JSON search API
client = new WebClient();
client.OpenReadCompleted += new OpenReadCompletedEventHandler(client_OpenReadCompleted);
}
private void MyMap_MouseClick(object sender, ESRI.ArcGIS.Map.MouseEventArgs e)
{
ESRI.ArcGIS.Geometry.MapPoint pt = e.MapPoint;
string xy = string.Format("{0},{1}", pt.X.ToString(NumberFormatInfo.InvariantInfo), pt.Y.ToString(NumberFormatInfo.InvariantInfo));
client.OpenReadAsync(new Uri("http://sampleserver1.arcgisonline.com/ArcGIS/rest/services/Locators/ESRI_Geocode_EU/GeocodeServer/reverseGeocode?location=" + xy + "&distance=0&f=pjson"));
Canvas.SetLeft(ResultsPane,e.ScreenPoint.X + 10);
Canvas.SetTop(ResultsPane,e.ScreenPoint.Y + 10);
GraphicsLayer graphicsLayer = MyMap.Layers["MyGraphicsLayer"] as GraphicsLayer;
Graphic graphic = new Graphic();
graphic.Symbol = DefaultMarkerSymbol;
graphic.Geometry = pt;
graphicsLayer.Graphics.Add(graphic);
}
void client_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
if (e.Error == null)
{
// Parse results into a JsonObject
results = (JsonObject)JsonObject.Load(e.Result);
// Update LINQ query
UpdateQuery();
ResultsPane.Visibility = Visibility.Visible;
}
}
void UpdateQuery()
{
if (results != null)
{
// Generate LINQ query based on JsonObject and user settings
var members = from address in results
where address.Key == "address"
select address;
// Databind the results to a UI control
resultList.DataContext = members;
}
}
private void btnClose_Click(object sender, RoutedEventArgs e)
{
ResultsPane.Visibility = Visibility.Collapsed;
GraphicsLayer graphicsLayer = MyMap.Layers["MyGraphicsLayer"] as GraphicsLayer;
graphicsLayer.ClearGraphics();
}
}
public class JsonFormatter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
string valueString = ((System.Collections.Generic.KeyValuePair<string,JsonValue>)value).Value[(string)parameter];
// JsonObject doesn't strip quotes from strings, so do that and return the result
return valueString.Replace("\"", "");
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}


XAML:


<UserControl x:Class="slWSJson.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:esri="clr-namespace:ESRI.ArcGIS;assembly=ESRI.ArcGIS"
    xmlns:esriConverters="clr-namespace:ESRI.ArcGIS.ValueConverters;assembly=ESRI.ArcGIS"
    xmlns:esriSymbols="clr-namespace:ESRI.ArcGIS.Symbols;assembly=ESRI.ArcGIS"
    xmlns:local="clr-namespace:slWSJson">
  <UserControl.Resources>
    <local:JsonFormatter x:Key="jsonFormatter"/>
  </UserControl.Resources>
  <Grid x:Name="LayoutRoot" Background="White">
    <Grid.Resources>
      <esriSymbols:SimpleMarkerSymbol x:Name="DefaultMarkerSymbol"  Color="Red" Size="12" Style="Circle" />
    </Grid.Resources>
    <esri:Map x:Name="MyMap" Background="White" Extent="-7.732,35.69,27.021,56.346" MouseClick="MyMap_MouseClick">
      <esri:Map.Layers>
        <esri:ArcGISTiledMapServiceLayer ID="BaseMapLayer" Url="http://server.arcgisonline.com/ArcGIS/rest/services/ESRI_StreetMap_World_2D/MapServer" />
        <esri:GraphicsLayer ID="MyGraphicsLayer">
        </esri:GraphicsLayer>
      </esri:Map.Layers>
    </esri:Map>
    <Canvas>
      <StackPanel x:Name="ResultsPane" Visibility="Collapsed" Width="300" Height="150">
        <Button x:Name="btnClose"  Click="btnClose_Click" Content="Close">
        </Button>
        <ListBox x:Name="resultList"  ItemsSource="{Binding}" Height="150">
          <ListBox.ItemTemplate>
            <DataTemplate>
              <StackPanel>
                <TextBlock FontSize="18" TextWrapping="Wrap">
                  <TextBlock.Text>
                    <Binding Converter="{StaticResource jsonFormatter}" ConverterParameter="Address"></Binding>
                  </TextBlock.Text>
                </TextBlock>
                <TextBlock FontSize="14">
                  <TextBlock.Text>
                    <Binding Converter="{StaticResource jsonFormatter}" ConverterParameter="City"></Binding>
                  </TextBlock.Text>
                </TextBlock>
                <TextBlock FontSize="14">
                  <TextBlock.Text>
                    <Binding Converter="{StaticResource jsonFormatter}" ConverterParameter="Postcode"></Binding>
                  </TextBlock.Text>
                </TextBlock>
                <TextBlock FontSize="14">
                  <TextBlock.Text>
                    <Binding Converter="{StaticResource jsonFormatter}" ConverterParameter="Country"></Binding>
                  </TextBlock.Text>
                </TextBlock>
                <TextBlock FontSize="14">
                  <TextBlock.Text>
                    <Binding Converter="{StaticResource jsonFormatter}" ConverterParameter="Loc_name"></Binding>
                  </TextBlock.Text>
                </TextBlock>
              </StackPanel>
            </DataTemplate>
          </ListBox.ItemTemplate>
        </ListBox>
      </StackPanel>
    </Canvas>
  </Grid>
</UserControl>







Scarica qui la soluzione