WPF – dynamiczne tworzenie i wczytywanie kodu XAML

2012-06-18

W dzisiejszym wpisie pokażę w jaki sposób dla danego obiektu WPF wygenerować kod XAML oraz jak taki kod wczytać dynamicznie podczas działania programu. W tym celu użyję klas XamlWriter i XamlReader. Klasa XamlWriter poprzez metodę Save umożliwia wygenerowanie (oraz ewentualne zapisanie do pliku) kodu XAML dla przekazanego w parametrze obiektu. Z kolei klasa XamlReader przy wykorzystaniu metody Load lub Parse pozwala na wczytanie kodu XAML i utworzenie na jego podstawie odpowiedniego obiektu (zwracany typ to object).

Aby zaprezentować działanie wspomnianych klas utworzyłem metody CreateXamlFile oraz LoadXamlFile. Pierwsza z nich tworzy trzy kontrolki (Label, TextBox, Button), następnie pojemnik StackPanel, po czym dodaje do niego utworzone wcześniej elementy. Na koniec poprzez klasę XamlWriter generowany jest kod XAML dla obiektu StackPanel i zapisywany do pliku:

private void CreateXamlFile()
{
    Label label = new Label
    {
        Name = "label1",
        Content = "Label",
        Height = 28,
        Width = 80,
        HorizontalContentAlignment = System.Windows.HorizontalAlignment.Center,
        FontSize = 16,
        Foreground = new SolidColorBrush(Colors.RoyalBlue),
        FontWeight = FontWeights.Bold,
        Margin = new Thickness(0, 20, 0, 0)
    };

    TextBox textBox = new TextBox
    {
        Name = "textBox1",
        Text = "TextBox",
        Height= 23,
        Width = 120,
        Background = new SolidColorBrush(Colors.GreenYellow),
        BorderBrush = new SolidColorBrush(Colors.Black),
        Margin = new Thickness(0, 20, 0, 0)
    };

    Button button = new Button
    {
        Name = "button1",
        Content = "Button",
        Height = 48,
        Width = 95,
        FontSize = 16,
        Foreground = new SolidColorBrush(Colors.White),
        Background = new SolidColorBrush(Colors.RoyalBlue),
        Margin = new Thickness(0, 20, 0, 0)
    };

    StackPanel stackPanel = new StackPanel
    {
        Name = "stackPanel1",
        Background = new SolidColorBrush(Colors.AliceBlue),
        Orientation = Orientation.Vertical
    };

    stackPanel.Children.Add(label);
    stackPanel.Children.Add(textBox);
    stackPanel.Children.Add(button);

    using (FileStream fs = new FileStream(@"D:\StackPanel.xaml", FileMode.Create))
    {
        System.Windows.Markup.XamlWriter.Save(stackPanel, fs);
    }

    /*
    Wygenerowanie kodu XAML i zwrócenie go w postaci string:
    string xamlCode = System.Windows.Markup.XamlWriter.Save(stackPanel);
    */
}

Jak widać w zakomentowanym fragmencie kodu, można skorzystać z przeciążonej metody Save w celu zwrócenia wygenerowanego kodu XAML jako string.

Zawartość utworzonego pliku StackPanel.xaml:

<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    Name="stackPanel1" Orientation="Vertical" Background="#FFF0F8FF">
    <Label Name="label1" Foreground="#FF4169E1" FontSize="16" FontWeight="Bold"
        HorizontalContentAlignment="Center" Width="80" Height="28" Margin="0,20,0,0">
        Label
    </Label>
    <TextBox Name="textBox1" BorderBrush="#FF000000" Background="#FFADFF2F"
        Width="120" Height="23" Margin="0,20,0,0">
        TextBox
    </TextBox>
    <Button Name="button1" Background="#FF4169E1" Foreground="#FFFFFFFF"
        FontSize="16" Width="95" Height="48" Margin="0,20,0,0">
        Button
    </Button>
</StackPanel>

Metoda LoadXamlFile przy wykorzystaniu klasy XamlReader odczytuje zapisany wcześniej kod XAML i zwraca zdefiniowany w nim obiekt StackPanel:

private object LoadXamlFile()
{
    object xamlObject;

    using (FileStream fs = new FileStream(@"D:\StackPanel.xaml", FileMode.Open))
    {
        xamlObject = System.Windows.Markup.XamlReader.Load(fs);
    }

    /*
    Utworzenie obiektu na podstawie kodu XAML przekazanego jako string:
    xamlObject = System.Windows.Markup.XamlReader.Parse(xamlCode);
    */

    return xamlObject;
}

W zakomentowanym kodzie znajduje się przykład użycia metody Parse, która zwraca obiekt utworzony na podstawie kodu XAML przekazanego w parametrze jako string.

W jaki sposób utworzyć obiekty na podstawie kodu XAML oraz wykorzystać je w programie podczas jego działania? Pokażę to na przykładzie wczytywania zawartości okna. W tym celu do projektu WPF dodałem puste okno (nazwałem je DynamicWindow), którego zawartość będzie ładowana dynamicznie z utworzonego wcześniej pliku StackPanel.xaml.

DynamicWindow.xaml:

<Window x:Class="XAMLDynamic.DynamicWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="DynamicWindow" Height="230" Width="255"
        WindowStartupLocation="CenterScreen">
</Window>

DynamicWindow.xaml.cs:

/// <summary>
/// Interaction logic for DynamicWindow.xaml
/// </summary>
public partial class DynamicWindow : Window
{
    public DynamicWindow(object content = null)
    {
        InitializeComponent();
        LoadContent(content);
    }

    private void LoadContent(object content)
    {
        if (content != null)
        {
            DependencyObject dependencyObject = content as DependencyObject;
            this.Content = dependencyObject;
            Button button = LogicalTreeHelper.FindLogicalNode(
                dependencyObject, "button1") as Button;
            if (button != null)
                button.Click += new RoutedEventHandler(button1_Click);
        }
    }

    private void button1_Click(object sender, RoutedEventArgs e)
    {
        MessageBox.Show("Click!!!");
    }
}

W Code-Behind utworzyłem metodę LoadContent, a do konstruktora dodałem parametr content oraz wywołanie tej metody. Jeżeli do konstruktora przekażemy określony obiekt, metoda LoadContent ustawi go jako zawartość tworzonego okna (this.Content). Dodatkowo w metodzie LoadContent pokazałem w jaki sposób obsłużyć zdarzenie w dynamicznie załadowanym obiekcie. Można to zrobić poprzez klasę LogicalTreeHelper i jej metodę FindLogicalNode, która w drzewie elementów przekazanego obiektu szuka elementu o podanej nazwie (w tym przypadku button1). Po odnalezieniu przycisku o nazwie button1 do jego zdarzenia Click podpinana jest metoda button1_Click.

Jeżeli utworzymy okno bez przekazania obiektu w parametrze konstruktora, zobaczymy pustą zawartość:

DynamicWindow dynamicWindow = new DynamicWindow();
dynamicWindow.ShowDialog();

Jeżeli przekażemy obiekt utworzony na podstawie kodu XAML wczytanego z pliku StackPanel.xaml (metoda LoadXamlFile), zobaczymy oczekiwaną zawartość okna i działające zdarzenie Click:

object content = LoadXamlFile();
DynamicWindow dynamicWindow = new DynamicWindow(content);
dynamicWindow.ShowDialog();

W zaprezentowanym przykładzie dynamicznie wczytany obiekt został wykorzystany jako zawartość okna. Oczywiście nic nie stoi na przeszkodzie aby wykorzystać go w inny sposób, np. dodać do kolekcji elementów istniejącego obiektu (Children.Add). Można również do pliku XAML zapisać całe okno, a następnie przy odczycie rzutować uzyskany obiekt na typ Window i otwierać bezpośrednio poprzez metodę Show lub ShowDialog.

Więcej informacji o opisywanych klasach można znaleźć na MSDN:  XAML Overview (WPF)XamlReaderXamlWriter

Reklamy

Posted on 2012-06-18, in .NET/C# and tagged , , , , , . Bookmark the permalink. 5 komentarzy.

  1. Nie wiem jak w WPF ale w SL XamlReader ma jakiś problem z kulturą (Zamiast stosować CurrentCulture stosuje kulturę en_US)
    Dodatkowo analiza XAML w runtime nie działa najszybciej.

    • XamlReader ma problemy ze znakami narodowymi jeżeli wczytywany plik nie jest w kodowaniu UTF-8. W przypadku plików UTF-8 nie miałem problemów z interpretacją ich zawartości.

      • Chodzi mi o to że jeżeli zrobisz Binding w XamlReaderze w której właściwość kontrolki typu string wiążesz z wartością wrażliwą na kulturę to otrzymujesz napis w formacie amerykańskim, a do Converterów w ostatnim parametr (typu CultureInfo) ma właściwości wskazujące na uruchomienie aplikacji na maszynie amerykańskiej.

      • Być może rozwiązaniem problemu będzie ustawienie właściwej „kultury” przy starcie aplikacji:

        protected override void OnStartup(StartupEventArgs e)
        {
        FrameworkElement.LanguageProperty.OverrideMetadata(typeof(FrameworkElement), new FrameworkPropertyMetadata(System.Windows.Markup.XmlLanguage.GetLanguage(System.Threading.Thread.CurrentThread.CurrentCulture.IetfLanguageTag)));

        base.OnStartup(e);
        }

        Kod zaczerpnięty z blogu Piotra Zielińskiego.

  1. Pingback: Podsumowanie 2012 « Developer notes

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Log Out / Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Log Out / Zmień )

Facebook photo

Komentujesz korzystając z konta Facebook. Log Out / Zmień )

Google+ photo

Komentujesz korzystając z konta Google+. Log Out / Zmień )

Connecting to %s

%d blogerów lubi to: