Category Archives: Silverlight

Jak pobrać adres IP serwera RIA Services z poziomu aplikacji klienckiej Silverlight?

Dzisiaj tylko króciutka notka ale mam nadzieję, że komuś się przyda. Ostatnio pisząc aplikację w Silverlight + RIA Services musiałem pobrać (w aplikacji SL) adres IP serwera na którym znajduje się RIA Service. Zrealizowałem to następująco:

System.Windows.Ria.Services.WebDomainClient<Services.DataContext.IDataServiceContract> domain;
domain = m_DataContext.DomainClient as System.Windows.Ria.Services.WebDomainClient<Services.DataContext.IDataServiceContract>;
string url = string.Format("http://{0}:{1}/",domain.ServiceUri.Host,domain.ServiceUri.Port));

Z okazji Wielkanocy –  życzę wszystkiego najlepszego ! 🙂

Wysyłanie plików na serwer w aplikacji Silverlight

Silverlight jest technologią odpowiedzialną za tworzenie warstwy prezentacji po stronie klienta. Wszelki kod wykonywany jest w przeglądarce zatem nie ma bezpośredniego dostępu do serwera www. Czasami jednak aplikacja musi wysyłać do serwera jakieś pliki np. graficzne (w przypadku modułu galerii zdjęć). Niestety ze względu na opisaną lokalność działania, Silverlight nie wspiera tego. Jedną z metod obejścia jest implementacja własnego HTTP handlera. Zacznijmy więc od stworzenia aplikacji Silverlight hostowanej za pomocą ASP .NET. Następnie dodajemy HTTP handler ( Add –> New Item –> Web –>Generic Handler), który będzie odpowiedzialny za zapis pliku na dysku.  Silverlight będzie wysyłał żądania zapisu pliku na serwerze właśnie   do tego handlera. Kod handler’a nie jest zbyt skomplikowany:

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class FileReceiver : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        string filename = context.Request.QueryString["filename"].ToString();
        string destFileName = "docelowa sciezka";
        using (FileStream fs = File.Create(destFileName))
        {
            SaveFile(context.Request.InputStream, fs, destFileName);
        }            
    }        
    private void SaveFile(Stream stream, FileStream fs)
    {
        byte[] buffer = new byte[4096];
        int bytesRead;
        while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) != 0)
        {
            fs.Write(buffer, 0, bytesRead);
        }
        fs.Close();                              
    }      
    public bool IsReusable
    {
        get
        {
            return false;
        }
    }
}

Jak widać handler po prostu odczytuje nazwę pliku zapisanego w parametrze filename (QueryString) a następnie zapisuje otrzymane dane na dysku. Jeśli użyliśmy szablonu Generic Handler już nic więcej po stronie serwera nie musimy dodawać ani konfigurować. Przejdźmy teraz do aplikacji klienckiej (Silverlight’a). W celu wysłania pliku będziemy wykorzystywać klasę WebClient:

Stream data = fileInfo.OpenRead();   
string address="http://localhosty:1354/FileReceiver.ashx?filename=nazwa_pliku";  
WebClient c = new WebClient();
c.OpenWriteCompleted += (sender, e) =>
  {      
      PushData(data, e.Result);
      e.Result.Close();
      data.Close();
  };
  
c.OpenWriteAsync(address);

gdzie PushData to:

private void PushData(Stream input, Stream output)
{
  byte[] buffer = new byte[4096];
  int bytesRead;

  while ((bytesRead = input.Read(buffer, 0, buffer.Length)) != 0)
  {
      output.Write(buffer, 0, bytesRead);
  }
}

Oczywiście na koniec należy dodać jeszcze jakiś TextBox oraz Button, który będzie wywoływał OpenFileDialog (wybieranie pliku).

Silverlight – lokalizacja aplikacji

Nowoczesne aplikacje web’owe często wymagają obsługi wielu języków. Silverlight podobnie jak ASP .NET wspiera mechanizm globalizacji za pomocą zasobów (resources files). Pliki zasobów są tak naprawdę zwykłymi plikami XML. Stanowi to ogromną zaletę – za pomocą zewnętrznego narzędzia odpowiednie osoby  mogą przygotować tłumaczenie interfejsu.

Najpierw należy stworzyć główny plik zasobów. Zawiera on tłumaczenia dla domyślnego języka. W przypadku gdy użytkownik pochodzi z kraju, którego język nie jest obsługiwany przez naszą aplikację to właśnie domyślne zasoby będą ładowane. Tworzymy więc plik zasobów za pomocą Add –> New Item –>Resources File. Nazwę pliku pozostawiamy domyślną:

image

Naszym domyślnym językiem będzie angielski. Dopisujemy więc przykładowy zasób o nazwie HELLO_WORLD_LABEL z wartością “Hello world!”:

image

Tworzymy drugi zasób odpowiedzialny za język polski. Jako nazwę podajemy “Resource1.pl-PL.resx”. Tym razem nazwa jest bardzo istotna ponieważ po niej właśnie ładowane są pliki w zależności od ustawień regionalnych użytkownika.

image

Podobnie jak w przypadku zasobu domyślnego, tworzymy wpis “HELLO_WORLD_LABEL” z przetłumaczoną zawartością (“Witaj świecie!”).

image

W tej chwili mamy już przygotowane zasoby. Przyszedł czas na wykorzystanie ich w programie. W pliku SL (MainPage.xaml) tworzymy pojedyncza kontrolkę Label:

<UserControl xmlns:dataInput="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data.Input"  x:Class="SilverlightApplication1.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">

    <Grid x:Name="LayoutRoot">
        <dataInput:Label x:Name="m_Label"></dataInput:Label>
    </Grid>
</UserControl>

Wykorzystanie zasobów za pomocą kodu jest bardzo proste. Wystarczy użyć automatycznie wygenerowaną klasę Resources1. Odpowiednie przypisanie można wykonać w konstruktorze klasy UserControl (MainPage.xaml):

public MainPage()
{
    InitializeComponent();
    m_Label.Content = Resource1.HELLO_WORLD_LABEL;
}

Na dzień dzisiejszy niestety trzeba dodatkowo ręcznie wyedytować plik projektu Silverlight. Otwieramy zatem SilverlightApplication1.csproj i znajdujemy element SupportedCultures. Wewnątrz tego elementu dopisujemy pl-PL:

<SupportedCultures>
      pl-PL
</SupportedCultures>

Ostatnią rzeczą, którą należy zrobić jest odpowiednie skonfigurowanie obiektu Silverlight w HTML. Należy przekazać dwa dodatkowe parametry – “culture”

oraz “uiculture”:

<object data="data:application/x-silverlight-2," type="application/x-silverlight-2" width="100%" height="100%">
      <param name="culture" value="pl-PL" />
      <param name="uiculture" value="pl-PL" />
      
      <param name="source" value="ClientBin/SilverlightApplication1.xap"/>
      <param name="onError" value="onSilverlightError" />
      <param name="background" value="white" />
      <param name="minRuntimeVersion" value="3.0.40818.0" />
      <param name="autoUpgrade" value="true" />
      <a href="http://go.microsoft.com/fwlink/?LinkID=149156&v=3.0.40818.0" style="text-decoration:none">
          <img src="http://go.microsoft.com/fwlink/?LinkId=108181" alt="Get Microsoft Silverlight" style="border-style:none"/>
      </a>
</object>

Wartość ustawiamy  w zależności od języka, który chcemy wykorzystać (w powyższym przypadku jest to język polski). Wartości odpowiadają zarówno za wygląd interfejsu (tłumaczenia) jak i za formatowanie walut, liczb, dat itp.

Po odpaleniu strony powinniśmy ujrzeć polską wersje aplikacji:

image

W celach testowych zmieńmy język na jakiś nieobsługiwany:

<object data="data:application/x-silverlight-2," type="application/x-silverlight-2" width="100%" height="100%">
      <param name="uiculture" value="pt-BR" />
      <param name="culture" value="pt-BR" />

      <param name="source" value="ClientBin/SilverlightApplication1.xap"/>
      <param name="onError" value="onSilverlightError" />
      <param name="background" value="white" />
      <param name="minRuntimeVersion" value="3.0.40818.0" />
      <param name="autoUpgrade" value="true" />
      <a href="http://go.microsoft.com/fwlink/?LinkID=149156&v=3.0.40818.0" style="text-decoration:none">
          <img src="http://go.microsoft.com/fwlink/?LinkId=108181" alt="Get Microsoft Silverlight" style="border-style:none"/>
      </a>
</object>

Po odpaleniu przeglądarki załaduje się domyślny plik zasobów (Resources1.resx) zawierający w naszym przypadku angielskie tłumaczenie:

image

Wiemy jak już wykorzystać zasoby z poziomu kodu. Interfejs jednak powinno definiować się w pliku XAML. Zasoby można podczepiać w XAMl za pomocą binding’u. Oto przykład:

<UserControl xmlns:dataInput="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data.Input"  x:Class="SilverlightApplication1.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    
    xmlns:local="clr-namespace:SilverlightApplication1"
             
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
    <UserControl.Resources>
        <local:Resource1 x:Key="ResourceHelper"/>        
    </UserControl.Resources>
  <Grid x:Name="LayoutRoot">
    <dataInput:Label Content="{Binding Path=HELLO_WORLD_LABEL,Source={StaticResource ResourceHelper}}" x:Name="m_Label"></dataInput:Label>        
  </Grid>
</UserControl>

W pliku (MainPage.xaml) nastąpiły 3 zmiany:

  1. Została podpięta przestrzeń nazw w której znajduje się klasa zasobów (Resource1):

    xmlns:local="clr-namespace:SilverlightApplication1"
  2. Aby móc korzystać z zasobu w XAML należy go zadeklarować i nadać klucz:

    <UserControl.Resources>

        <local:Resource1 x:Key="ResourceHelper"/>       
    </UserControl.Resources>

  3. Następnie za pomocą statycznego wiązania wystarczy podpiąć HELLO_WORLD_LABEL:

    <dataInput:Label Content="{Binding Path=HELLO_WORLD_LABEL,Source={StaticResource ResourceHelper}}" x:Name="m_Label"></dataInput:Label>       

Ponadto należy zmienić modyfikatory internal na public w pliku Resources1.Designer.cs. Bez tego binding nie zadziała!

Kompletny kod źródłowy znajduje się tutaj.

BusyIndicator w Silverlight

W Silverlight toolkit można znaleźć bardzo przydatną kontrolkę – BusyIndicator(poprzednia nazwa Activity). Służy ona do zasygnalizowania użytkownikowi, że jakaś operacja jest wykonywana. Na początek screen z gotowej aplikacji:

image

Co warto podkreślić, podczas stanu aktywnego kontrolki użytkownik nie może modyfikować danych, które przedstawia interfejs. Kontrolka działa w trybie modalnym – wszystko w tle jest zablokowane i automatycznie przyciemnione.

Po zainstalowaniu bibliotek z Silverlight toolkit, najlepiej dodać je do toolbox’a . W tym celu klikamy prawym przyciskiem myszki na oknie toolbox a następnie wybieramy “Choose Items”. W oknie przechodzimy do “Silverlight components” i klikamy w przycisk “Browse”. Przechodzimy do katalogu z zainstalowanymi bibliotekami  a następnie zaznaczamy System.Windows.Controls.Toolkit.dll.

Użycie kontrolki sprowadza się do ustawienia właściwości IsBusy na true w przypadku gdy chcemy zasygnalizować użytkownik wykonywaną operację:

<controlsToolkit:BusyIndicator IsBusy="True">

</controlsToolkit:BusyIndicator>

W praktyce oczywiście będziemy korzystać z wiązania danych i jako wartość IsBusy podamy np. {Binding Path=IsBusy}.

Następną istotną sprawą jest fakt, że w kontrolce BusyIndicator możemy zagnieżdżać inne kontrolki, które korzystają ze źródła danych. Wtedy IsBusy przeważnie jest już ustawiane automatycznie przez zagnieżdżoną kontrolkę.

Wygląd i zachowanie BusyIndictator możemy zmieniać dowolnie – podobnie jak resztę kontrolek WPF.

ComboBox w DataGrid oraz brak wsparcia SelectedValue w Silverlight

Siliverlight niestety nie jeszcze tak potężną biblioteką jak WPF. Czasami proste rzeczy wymagają większego nakładu pracy niż w przypadku tworzenia aplikacji w WPF.

Rozważmy sytuację, w której mamy listę telefonów do danej osoby.Telefon definiujemy poprzez jego numer oraz przydzieloną kategorię(dom, praca itp). W DataGrid będą więc 2 kolumny: jedna typu TextBox dla numeru oraz druga typu ComboBox dla kategorii. DataGrid będzie przedstawiał listę telefonów opisanych klasą Phone:

class Phone
{
    public string Value{get;set;}
    public PhoneType PhoneType{get;set;}
}

gdzie PhoneType to:

class PhoneType
{
    public string Value{get;set;}// nazwa kategorii
    public Guid ID_PHONE_TYPE{get;set;}
}

Zrzut z gotowej kontrolki DataGrid:

Clipboard01

W wpf kod byłby bardzo prosty a mianowicie:

<data:DataGrid   Name="m_Phones" ItemsSource="{Binding Path=Contact.Phones}" AutoGenerateColumns="False">
    <data:DataGrid.Columns>
                           
        <data:DataGridTextColumn Header="Phone" Binding="{Binding Path=Value}"/>
                            
            <data:DataGridTemplateColumn Header="PhoneGroup">
                <data:DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <ComboBox SelectedValuePath="ID_PHONE_TYPE" SelectedValue="{Binding Path=ID_PHONE_TYPE"  DisplayMemberPath="Value" ItemsSource="{StaticResource AllPhoneTypes}">
                           </ComboBox>
                    </DataTemplate>
                </data:DataGridTemplateColumn.CellTemplate>

        </data:DataGridTemplateColumn>

    </data:DataGrid.Columns>
</data:DataGrid>

SelectedValuePath wskazuje na identyfikator item’a w ComboBox. SelectedValue służy do zaznaczania aktualnej kategorii.Wszystko w wpf ładnie będzie nam się odświeżać. Załadowane telefony automatycznie będą zaznaczać kategorię w ComboBox.

Niestety w Silverlight brakuje właściwości SelectedValue oraz SelectedValuePath.Wszystko co możemy zrobić za pomocą xaml to:

<data:DataGrid   Name="m_Phones" ItemsSource="{Binding Path=Contact.Phones}" AutoGenerateColumns="False">
    <data:DataGrid.Columns>
                           
        <data:DataGridTextColumn Header="Phone" Binding="{Binding Path=Value}"/>
                            
            <data:DataGridTemplateColumn Header="PhoneGroup">
                <data:DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <ComboBox DisplayMemberPath="Value"    ItemsSource="{StaticResource AllPhoneTypes}">
                           </ComboBox>
                    </DataTemplate>
                </data:DataGridTemplateColumn.CellTemplate>

        </data:DataGridTemplateColumn>

    </data:DataGrid.Columns>
</data:DataGrid>

W SL do dyspozycji mamy wyłącznie właściwość SelectedItem jeśli chodzi o zaznaczanie pozycji w ComboBox. Niestety ItemsSource dla ComboBox pochodzi z innego źródła niż aktualnie wybrana kategoria. Nie możemy zatem użyć poniższego kodu:

<ComboBox SelectedItem="{Binding Path=PhoneType}" DisplayMemberPath="Value"  ItemsSource="{StaticResource AllPhoneTypes}"/>

Musimy powiązać SelectedItem za pomocą ID_PHONE_TYPE a nie instancji klasy PhoneType.Problem polega na tym, że SelectedItem spodziewa się typu PhoneType a nie GUID. Co więc możemy zrobić?

Na szczęście  SilverLight wspiera konwertery IValueConverter. Umożliwiają one podczas bindingu zamianę jednej wartości na drugą. W naszym przypadku zamienimy GUID na klasę PhoneType. Zatem do dzieła, zacznijmy od implementacji tego konwertera:

public class PhoneTypeConverter : IValueConverter
{
    public ObservableCollection<PhoneType> ItemsSource { get; set; }
    
    public object Convert(object value,
    System.Type targetType,
    object parameter,
    System.Globalization.CultureInfo culture)
    {
        Guid phoneTypeId = (Guid)value;
        PhoneType phoneType = null;
        
        foreach (PhoneType item in ItemsSource)
        {
            if (item.ID_PHONE_TYPE == phoneTypeId)
            {
                phoneType = item;
            }
        }
        return phoneType;
    }
        
    public object ConvertBack(object value,
    System.Type targetType, object parameter,
    System.Globalization.CultureInfo culture)
    {
        PhoneType phoneType = value as PhoneType;
        Guid phoneTypeId = Guid.Empty;
        phoneTypeId = phoneType.ID_PHONE_TYPE;
        return phoneTypeId;
    }
}

Konwerter zawiera dwie metody Convert(Guid –> PhoneType) oraz ConvertBack(PhoneType –> Guid).Teraz możemy już użyć SelectedItem:

<ComboBox SelectedItem="{Binding Path=ID_PHONE_TYPE,Converter={StaticResource PhoneTypeConverter}}" DisplayMemberPath="Value"  ItemsSource="{StaticResource AllPhoneTypes}"/>

Konwerter zamieni Guid(ID_PHONE_TYPE) na odpowiednią  instancję PhoneType, która jest już podłączona do ItemsSource. Ostatnią sprawą jest zdefiniowanie zasobu PhoneTypeConverter:

<UserControl.Resources>
   <local:PhoneTypeConverter x:Key="PhoneTypeConverter" ItemsSource={StaticResource AllPhoneTypes} />        
</UserControl.Resources>

Takim to sposobem poradziliśmy sobie z brakiem wsparcia dla SelectedValue w Silverlight. Mam nadzieję, że oszczędziłem komuś męczarni z tym – samemu sporo sie nakombinowałem zanim udało mi się osiągnąć zamierzony  efekt.Dodam, że SilverLight 4 Beta posiada już właściwość SelectedValue więc takie scenariusze będą łatwiejsze w implementacji.

Silverlight i brak wsparcia dla x:Static

Programiści WPF z pewnością kojarzą makro x:Static służące do bindowania m.in. stałych. Bardzo przydatne jest to w przypadku gdy mamy klasę stałych, którą chcemy przypisać pewnym właściościom kontrolek. W PRISM, wykorzystuje się to do definiowania nazw regionów:

<ItemsControl regions:RegionManager.RegionName="{x:Static slbase:RegionNames.NoteView}">

Dzięki takiemu rozwiązaniu nie musze martwić się o aktualziacje nazwy regionu w pliku xaml.

Ostatnio piszę projekt w Silverlight i spotkało mnie niemiłe zaskoczenie gdy okazało się, że SL nie wspiera takiego makra. W przykładach dołączonych do frameworku PRISM autorzy na sztywno podają nazwy. Znalazłem jednak lepsze moim zdaniem rozwiązanie. Możemy uzyć zasobów a mianowicie:

<controls:TabControl cal:RegionManager.RegionName="{Binding Note, Source={StaticResource Regions}}">

W powyższym kodzie bindujemy właściwość Note do RegionName. Wszystko realizowane jest również w sposób statyczny. Wystarczy, że zdefiniujemy jeszcze w ResourceDictionary następujący znacznik:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:slbase="clr-namespace:SlBase.Mvvm;assembly=SlBase">

    <slbase:Regions x:Key="RegionsBase"/>
</ResourceDictionary>

Nie jest to rozwiązanie idealne ale wystarczające. Zasoby możemy zdefiniować w osobnym pliku i  dołączać je w ramach potrzeb co redukuje ilość kodu.