1

I want to make app which deserialized data from my xml file to class structure. I prepared classes by 'Paste XML as classes' tool, however everything is made on common fields or tables and when I tried to change it for List or ObservableCollections serializator stopped load xml document properly.

What i want to do next is possibilty to choose from treeview for instance some element, edit it, and save to xml file again. I don't want to do that on .xml directly. this is a sample of my XML:

    <plan xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="schema.xsd">
<nagłówek>
<autorzy>
<nazwa>Autorzy:</nazwa>
<autor atr="one">
<numer>222</numer>
<imię>Rust</imię>
<nazwisko>Snow</nazwisko>
</autor>

<autor>
<numer>111</numer>
<imię>Ian</imię>
<nazwisko>Nower</nazwisko>
</autor>
</autorzy>
</nagłówek>
...

Here are exemple of classes

/// <remarks/>
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)]
[System.Xml.Serialization.XmlRootAttribute(Namespace = "", IsNullable = false)]
public partial class plan
{

    private planNagłówek nagłówekField;

    private planGłówny głównyField;

    /// <remarks/>
    public planNagłówek nagłówek
    {
        get
        {
            return this.nagłówekField;
        }
        set
        {
            this.nagłówekField = value;
        }
    }

    /// <remarks/>
    public planGłówny główny
    {
        get
        {
            return this.głównyField;
        }
        set
        {
            this.głównyField = value;
        }
    }
}

/// <remarks/>
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)]
public partial class planNagłówek 
{

    private planNagłówekAutorzy autorzyField;

    /// <remarks/>
    public planNagłówekAutorzy autorzy
    {
        get
        {
            return this.autorzyField;
        }
        set
        {
            this.autorzyField = value;
        }
    }
}

and how i load xml:

// Create an instance of the XmlSerializer specifying type and namespace.
            XmlSerializer serializer = new XmlSerializer(typeof(XML.plan));
            // A FileStream is needed to read the XML document.
            FileStream fs = new FileStream("...somepath.../Untitled4.xml", FileMode.Open);
            XmlReader reader = XmlReader.Create(fs);

            // Use the Deserialize method to restore the object's state.
            i = (XML.plan)serializer.Deserialize(reader);
            fs.Close();    

Here is what I've got plus my xml file, maybe this will help you help me:) https://drive.google.com/file/d/0B0wPodV30rnJSVA1ckVxWldDRDA/view

Tom
  • 43
  • 7
  • What part of that task are you having trouble with? – Moti Azu Jan 06 '15 at 15:25
  • Well. I want to bind this structure of classes to treeview - and that is my goal for now. I cant do it without changes in classes and that is causing trouble to serializer. I am trying now with Hierarchical Data Template -> http://www.codemag.com/Article/1401031 – Tom Jan 06 '15 at 16:05
  • Why do you think you can't bind the tree to these classes? You got the right direction, `HierarchicalDataTemplate` is the way to go here. – Moti Azu Jan 06 '15 at 16:07
  • But as I see, I need some classes which will be list holding another elements. And - on the beginning - I have problem what to do with 'plan' class. It holds another elements, but they are single. Making list structure here seems to be pointless. I just add that I'm total beginner – Tom Jan 06 '15 at 16:15
  • XML serialization does *not* work with WPF out of the box. The serializer does not like the necessary `INotifyPropertyChanged` interface or `ObservableCollection`s and so you will need to do some manual work to fulfil your requirements. By that, I mean that you will not be able to use your generated classes with WPF, nor will you be able to serialize your classes that work correctly in WPF. So just iterate through your deserialized generated classes and populate some new WPF friendly classes... then you'll have more luck. – Sheridan Jan 06 '15 at 16:31
  • But how I should load xml document to them if I make them 'WPF friendly'? – Tom Jan 06 '15 at 16:34
  • This article describes how to bind an XDocument directly to a WPF tree: http://www.codeproject.com/Articles/317766/Displaying-XML-in-a-WPF-TreeView – dbc Jan 06 '15 at 19:20
  • yeah, but I have to bind class structure based on xml – Tom Jan 06 '15 at 19:29
  • I took your XML, generated c# classes, manually implemented `INotifyPropertyChanged`, and converted all the arrays to `ObservableCollection` -- and your xml serializes and deserializes successfully. `XmlSerializer` doesn't appear to have any problem with doing this. Should I put them in an answer? – dbc Jan 06 '15 at 20:06
  • Ok, I tried to make some ObservableCollection classes, maybe that was stupid idea. Did you implement `INotifyPropertyChanged` in every class? And main problem - how to properly bind it to treeview? edit: Of course, it will be helpful – Tom Jan 06 '15 at 20:27

1 Answers1

1

To display a hierarchy of classes in a WPF TreeView, in XAML you need to define a HierarchicalDataTemplate corresponding to each type of class in your hierarchy that might have children, and a DataTemplate corresponding to each type of class in your hierarchy that will not have children. Each data template should define a single framework element (TextBlock, for instance, or a container control such as a DockPanel with any number of embedded framework elements as children) that will display the data of that type of class in the tree, with appropriate bindings.

First, auto-generate classes for your XML using xsd.exe. To simply display the data in the tree you do not need to implement INotifyPropertyChanged or use an ObservableCollection<T> for children:

/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "2.0.50727.3038")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)]
[System.Xml.Serialization.XmlRootAttribute(Namespace = "", IsNullable = false)]
public partial class plan
{

    private planNagłówek[] itemsField;

    /// <remarks/>
    [System.Xml.Serialization.XmlElementAttribute("nagłówek", Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
    public planNagłówek[] Items
    {
        get
        {
            return this.itemsField;
        }
        set
        {
            this.itemsField = value;
        }
    }
}

/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "2.0.50727.3038")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)]
public partial class planNagłówek
{

    private planNagłówekAutorzy[] autorzyField;

    /// <remarks/>
    [System.Xml.Serialization.XmlElementAttribute("autorzy", Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
    public planNagłówekAutorzy[] autorzy
    {
        get
        {
            return this.autorzyField;
        }
        set
        {
            this.autorzyField = value;
        }
    }
}

/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "2.0.50727.3038")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)]
public partial class planNagłówekAutorzy
{

    private string nazwaField;

    private planNagłówekAutorzyAutor[] autorField;

    /// <remarks/>
    [System.Xml.Serialization.XmlElementAttribute(Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
    public string nazwa
    {
        get
        {
            return this.nazwaField;
        }
        set
        {
            this.nazwaField = value;
        }
    }

    /// <remarks/>
    [System.Xml.Serialization.XmlElementAttribute("autor", Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
    public planNagłówekAutorzyAutor[] autor
    {
        get
        {
            return this.autorField;
        }
        set
        {
            this.autorField = value;
        }
    }
}

/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "2.0.50727.3038")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)]
public partial class planNagłówekAutorzyAutor
{

    private string numerField;

    private string imięField;

    private string nazwiskoField;

    private string atrField;

    /// <remarks/>
    [System.Xml.Serialization.XmlElementAttribute(Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
    public string numer
    {
        get
        {
            return this.numerField;
        }
        set
        {
            this.numerField = value;
        }
    }

    /// <remarks/>
    [System.Xml.Serialization.XmlElementAttribute(Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
    public string imię
    {
        get
        {
            return this.imięField;
        }
        set
        {
            this.imięField = value;
        }
    }

    /// <remarks/>
    [System.Xml.Serialization.XmlElementAttribute(Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
    public string nazwisko
    {
        get
        {
            return this.nazwiskoField;
        }
        set
        {
            this.nazwiskoField = value;
        }
    }

    /// <remarks/>
    [System.Xml.Serialization.XmlAttributeAttribute()]
    public string atr
    {
        get
        {
            return this.atrField;
        }
        set
        {
            this.atrField = value;
        }
    }
}

Next, define a user interface in XAML in which to display these classes, manually creating appropriate data templates for each level:

<Window x:Class="WpfTreeViewNew.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:w="clr-namespace:WpfTreeViewNew"
    Title="Window1" Height="300" Width="600">
    <Window.Resources>
        <HierarchicalDataTemplate  DataType="{x:Type w:plan}" ItemsSource="{Binding Path=Items}">
            <TextBlock Text="Plan">
            </TextBlock>
        </HierarchicalDataTemplate >
        <HierarchicalDataTemplate  DataType="{x:Type w:planNagłówek}" ItemsSource="{Binding Path=autorzy}">
            <TextBlock Text="Nagłówek">
            </TextBlock>
        </HierarchicalDataTemplate >
        <HierarchicalDataTemplate  DataType="{x:Type w:planNagłówekAutorzy}" ItemsSource="{Binding Path=autor}">
            <TextBlock Text="{Binding Path=nazwa}"/>
        </HierarchicalDataTemplate >
        <DataTemplate  DataType="{x:Type w:planNagłówekAutorzyAutor}">
            <Border BorderBrush="Gray" BorderThickness="1" MinWidth="300">
                <StackPanel Orientation="Horizontal">
                    <TextBlock Margin="3" Text="{Binding Path=atr}"/>
                    <TextBlock Margin="3" Text="{Binding Path=numer}"/>
                    <TextBlock Margin="3" Text="{Binding Path=imię}"/>
                    <TextBlock Margin="3" Text="{Binding Path=nazwisko}"/>
                </StackPanel>
            </Border>
        </DataTemplate >
    </Window.Resources>
        <Grid DockPanel.Dock="Bottom">
            <TreeView Margin="3" Name="treeView1">
                <TreeView.ItemContainerStyle>
                    <Style TargetType="{x:Type TreeViewItem}">
                        <Setter Property="IsExpanded" Value="True" />
                    </Style>
                </TreeView.ItemContainerStyle>
            </TreeView>
        </Grid>
</Window>

Finally, load the data programmatically, for instance on startup:

public partial class Window1 : Window
{
    public Window1()
    {
        InitializeComponent();

        string xml = @"<plan xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xsi:noNamespaceSchemaLocation=""schema.xsd"">
  <nagłówek>
    <autorzy>
      <nazwa>Autorzy:</nazwa>
      <autor atr=""one"">
        <numer>222</numer>
        <imię>Rust</imię>
        <nazwisko>Snow</nazwisko>
      </autor>

      <autor>
        <numer>111</numer>
        <imię>Ian</imię>
        <nazwisko>Nower</nazwisko>
      </autor>
    </autorzy>
  </nagłówek>
</plan>
";
        var plan = XmlSerializationHelper.LoadFromXML<plan>(xml);
        var xml2 = plan.GetXml();
        Debug.WriteLine(xml2); // For testing

        var children = new List<plan>();
        children.Add(plan);

        treeView1.Items.Clear();
        treeView1.ItemsSource = children;
    }
}

This produces something that looks like the following:

enter image description here

You will want to replace each template with something more beautiful.

Honestly, after grinding all this out I now believe the WinForms tree may be easier to work with.

Update - Editing

Re-reading your question, I see your requirement is to allow the user to load, edit in tree, and save the XML. This is more complicated than just loading. Here are the steps:

First, add custom routed UI commands for loading and saving XML:

public static class CustomCommands
{
    public static readonly RoutedUICommand LoadXMLCommand = new RoutedUICommand("Load XML", "LoadXML", typeof(Window1));

    public static readonly RoutedUICommand SaveXMLCommand = new RoutedUICommand("Save XML", "SaveXML", typeof(Window1));
}

Next, add the actual c# logic in your Window1 class for these actions:

public partial class Window1 : Window
{
    public Window1()
    {
        InitializeComponent();
    }

    private void ExecutedLoadXML(object sender, ExecutedRoutedEventArgs e)
    {
        string xml = @"<plan xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xsi:noNamespaceSchemaLocation=""schema.xsd"">
  <nagłówek>
    <autorzy>
      <nazwa>Autorzy:</nazwa>
      <autor atr=""one"">
        <numer>222</numer>
        <imię>Rust</imię>
        <nazwisko>Snow</nazwisko>
      </autor>

      <autor>
        <numer>111</numer>
        <imię>Ian</imię>
        <nazwisko>Nower</nazwisko>
      </autor>
    </autorzy>
  </nagłówek>
</plan>
";
        var plan = XmlSerializationHelper.LoadFromXML<plan>(xml);

        var children = new List<plan>();
        children.Add(plan);

        treeView1.ItemsSource = null;
        treeView1.Items.Clear();
        treeView1.ItemsSource = children;
    }

    private void ExecutedSaveXML(object sender, ExecutedRoutedEventArgs e)
    {
        var planList = treeView1.ItemsSource as IList<plan>;
        if (planList != null && planList.Count > 0)
        {
            // Kludge to force pending edits to update
            treeView1.Focus();
            // Replace with actual save code!
            Debug.WriteLine(planList[0].GetXml());
        }
    }
}

As you can see I'm just loading from a hardcoded string, and saving by doing a debug writeline. You will want to replace these with the real logic.

Next, in XAML, add the commands defined above to <Window.CommandBindings>

Then, in XAML add a ToolBarTray and ToolBar with buttons to load and save XML, and bind the buttons to the commands you added to the CommandBindings above.

Finally, in XAML, for each DataTemplate or HierarchicalDataTemplate that contains a data field, replace the TextBlock for that field with an appropriate framework element for editing. Add any labels as desired as additional TextBlocks, and wrap them all up in a container such as a Grid.

Here is something that works:

<Window x:Class="WpfTreeViewNew.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:w="clr-namespace:WpfTreeViewNew"
    Title="Window1" Height="300" Width="600">
    <Window.CommandBindings>
        <CommandBinding Command="w:CustomCommands.LoadXMLCommand" Executed="ExecutedLoadXML"/>
        <CommandBinding Command="w:CustomCommands.SaveXMLCommand" Executed="ExecutedSaveXML"/>
    </Window.CommandBindings>
    <Window.Resources>
        <HierarchicalDataTemplate  DataType="{x:Type w:plan}" ItemsSource="{Binding Path=Items}">
            <TextBlock Text="Plan">
            </TextBlock>
        </HierarchicalDataTemplate >
        <HierarchicalDataTemplate  DataType="{x:Type w:planNagłówek}" ItemsSource="{Binding Path=autorzy}">
            <TextBlock Text="Nagłówek">
            </TextBlock>
        </HierarchicalDataTemplate >
        <HierarchicalDataTemplate  DataType="{x:Type w:planNagłówekAutorzy}" ItemsSource="{Binding Path=autor}">
            <Grid Margin="3" MinWidth="300">
                <Grid.RowDefinitions>
                    <RowDefinition />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition />
                    <ColumnDefinition />
                </Grid.ColumnDefinitions>
                <TextBlock Text="nazwa:" Grid.Column="0" Grid.Row="0"/>
                <TextBox Text="{Binding Path=nazwa, Mode=TwoWay}" Grid.Column="1" Grid.Row="0"/>
            </Grid>
        </HierarchicalDataTemplate >
        <DataTemplate  DataType="{x:Type w:planNagłówekAutorzyAutor}">
            <Border BorderBrush="Gray" BorderThickness="1" MinWidth="300">
                <Grid Margin="3" >
                    <Grid.RowDefinitions>
                        <RowDefinition />
                        <RowDefinition />
                        <RowDefinition />
                        <RowDefinition />
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition />
                        <ColumnDefinition />
                    </Grid.ColumnDefinitions>
                    <TextBlock Text="atr:" Grid.Column="0" Grid.Row="0"/>
                    <TextBox Text="{Binding Path=atr, Mode=TwoWay}" Grid.Column="1" Grid.Row="0"/>
                    <TextBlock Text="numer:" Grid.Column="0" Grid.Row="1"/>
                    <TextBox Text="{Binding Path=numer, Mode=TwoWay}" Grid.Column="1" Grid.Row="1"/>
                    <TextBlock Text="imię:" Grid.Column="0" Grid.Row="2"/>
                    <TextBox Text="{Binding Path=imię, Mode=TwoWay}" Grid.Column="1" Grid.Row="2"/>
                    <TextBlock Text="nazwisko:" Grid.Column="0" Grid.Row="3"/>
                    <TextBox Text="{Binding Path=nazwisko, Mode=TwoWay}" Grid.Column="1" Grid.Row="3"/>
                </Grid>
            </Border>
        </DataTemplate >
    </Window.Resources>
    <DockPanel>
        <ToolBarTray DockPanel.Dock="Top">
            <ToolBar>
                <Button Command="w:CustomCommands.LoadXMLCommand" Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"/>
                <Button Command="w:CustomCommands.SaveXMLCommand" Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"/>
            </ToolBar>
        </ToolBarTray>
        <Grid DockPanel.Dock="Bottom">
            <TreeView Margin="3" Name="treeView1">
                <TreeView.ItemContainerStyle>
                    <Style TargetType="{x:Type TreeViewItem}">
                        <Setter Property="IsExpanded" Value="True" />
                    </Style>
                </TreeView.ItemContainerStyle>
            </TreeView>
        </Grid>
    </DockPanel>
</Window>

And the UI it produces looks like:

enter image description here

I'm not a UI designer so you'll want to play with this to get something more beautiful.

Update 2

GetXML() extension method:

public static class XmlSerializationHelper
{
    public static string GetXml<T>(T obj, XmlSerializer serializer, bool omitStandardNamespaces)
    {
        using (var textWriter = new StringWriter())
        {
            XmlWriterSettings settings = new XmlWriterSettings();
            settings.Indent = true;        // For cosmetic purposes.
            settings.IndentChars = "    "; // For cosmetic purposes.
            using (var xmlWriter = XmlWriter.Create(textWriter, settings))
            {
                if (omitStandardNamespaces)
                {
                    XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
                    ns.Add("", ""); // Disable the xmlns:xsi and xmlns:xsd lines.
                    serializer.Serialize(xmlWriter, obj, ns);
                }
                else
                {
                    serializer.Serialize(xmlWriter, obj);
                }
            }
            return textWriter.ToString();
        }
    }

    public static string GetXml<T>(this T obj, bool omitNamespace)
    {
        XmlSerializer serializer = new XmlSerializer(obj.GetType());
        return GetXml(obj, serializer, omitNamespace);
    }

    public static string GetXml<T>(this T obj)
    {
        return GetXml(obj, false);
    }

    public static T LoadFromXML<T>(this string xmlString)
    {
        return xmlString.LoadFromXML<T>(new XmlSerializer(typeof(T)));
    }

    public static T LoadFromXML<T>(this string xmlString, XmlSerializer serial)
    {
        T returnValue = default(T);

        using (StringReader reader = new StringReader(xmlString))
        {
            object result = serial.Deserialize(reader);
            if (result is T)
            {
                returnValue = (T)result;
            }
        }
        return returnValue;
    }

    public static T LoadFromFile<T>(string filename)
    {
        XmlSerializer serial = new XmlSerializer(typeof(T));
        try
        {
            using (var fs = new FileStream(filename, FileMode.Open))
            {
                object result = serial.Deserialize(fs);
                if (result is T)
                {
                    return (T)result;
                }
            }
        }
        catch (Exception ex)
        {
            Debug.WriteLine(ex.ToString());
            throw;
        }
        return default(T);
    }
}
dbc
  • 104,963
  • 20
  • 228
  • 340
  • 1
    Wow, I'd never figure that out. I will look closely to that tommorow and for sure I will have some questions for you, but for now thank you, I really appreciate it. – Tom Jan 06 '15 at 23:45
  • OK, I'm back in home and started getting it I guess, now I am doing first 'part' of your answear and try with my xml file from the link I added to my first post. Can you tell me how your GetXML method in 'plan' class looks like? – Tom Jan 07 '15 at 17:02
  • @Tom - `GetXML()` is an extension method that serializes a class to XML and stores it in a string. Since the question wasn't about serializing or deserializing, I didn't include it. – dbc Jan 07 '15 at 18:39
  • This method is much more 'elastic' than mine, thanks I finished matching my document to class structure (step by step), I had to change it (the structure) a little and it took me a while, but it wasn't so hard as I thought. Loading and saving is working - no problems here. Tommorow I will add the ability to edit document - I have to read about routed UI commands, because I don't quite understand it. Next I will try to give it some better look. I also want to make nodes editable but I'll leave it as a optional for a future. – Tom Jan 08 '15 at 00:51
  • @Tom - you didn't put that in your question so it's not something I thought about. You could update your question, or better yet, ask a new question, since the preferred format on stackoverflow is [one question per question](https://meta.stackexchange.com/questions/39223/one-post-with-multiple-questions-or-multiple-posts). That being said, see here: http://stackoverflow.com/questions/3673173/wpf-treeview-databinding-hierarchal-data-with-mixed-types – dbc Jan 08 '15 at 19:19
  • @Tom - see also here: http://xinyustudio.wordpress.com/2012/09/27/wpf-treeview-data-binding-of-heterogeneous-types-of-data-datatemplate-and-hierarchicaldatatemplate/ – dbc Jan 08 '15 at 19:34
  • Ok, I've done editing, again I had to change structure. This is current version [link]https://drive.google.com/file/d/0B0wPodV30rnJNFJlU1c4TnNaamc/view Now I want to connect classes keeping same elements. I want make it recognizing by ID. For instance - element 'pracownik' from 'lista_pracowników' and 'prowadzący' from 'zajęcia' with the same ID is the same, but I totally don't know how to make it serializable. Should I ask new question? – Tom Jan 08 '15 at 23:40
  • @Tom - I'd recommend asking a new question, since this question is about how to edit a class hierarchy in a `TreeView`. Also, if this question has been answered, please do [mark it as such](https://meta.stackexchange.com/questions/5234/how-does-accepting-an-answer-work) – dbc Jan 08 '15 at 23:50
  • 1
    I will attempt to achieve it with `DataContractSerializer` and if I 'll fail (which is proably) ask a new question. Thank you @dbc for your time and patience – Tom Jan 09 '15 at 00:09