I am attempting to create a tool from WPF that creates a simple XML document by inputting data into a text box, then clicking a button to enter that data into the XML document. Other than the root element, I will have a step element with a child sub step element such as this:
<root>
<step id="1">
<P>some instructions</p>
<step id="1.1">
<p>some additional instructions</p>
</step>
</step>
<step id="2">
<p>second set of instructions</p>
<step id="2.1">
<p>additional instructions for step 2</p>
</step>
</step>
I have been able to add the parent steps, however all of my sub steps fall under the first parent step:
<root>
<step id="1">
<step id="1.1">
<step id="2.1">
<step id="3.1">
<step id="2">
<step id="3">
I am trying to use XPath to insert my sub steps into the proper parent-steps. However I am receiving the error:
"XAttribute does not contain a definition for 'add' and no extension method 'add' accepting a first argument type 'XAttribute' could be found."
I have searched extensively and am unable to come up with a solution, hopefully this makes sense and someone could give me a hand. Thank you in advance. Here is my code:
using System.Windows;
using System.Xml.Linq;
using System.Xml.XPath;
namespace XMLwpfTest1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
XDocument doc = new XDocument(
new XElement("Content",
new XElement("Title", "DM Title"))
);
string par = "par-";
int counter = 1;
int counter2 = 1;
string stepNumber = "";
public MainWindow()
{
InitializeComponent();
}
private void create_Click(object sender, RoutedEventArgs e)
{
doc.Element("Content").Add(
new XElement("proceduralStep",
new XAttribute("id", par + counter.ToString("D4")),
new XElement("para",
dataBox.Text)));
stepNumber = par + counter.ToString("D4");
counter += 1;
dataBox.Clear();
counter2 = 1;
}
private void createSubStep_Click(object sender, RoutedEventArgs e)
{
var addStep = doc.XPathSelectElement("Content/proceduralStep").Attribute(stepNumber);
addStep.add(
new XElement("proceduralStep",
new XAttribute("id", stepNumber + "-" + counter2.ToString("D4")),
new XElement("para",
subDataBox.Text)));
counter2 += 1;
subDataBox.Clear();
}
private void save_Click(object sender, RoutedEventArgs e)
{
doc.Save(fileName.Text + ".xml");
savedLabel.Visibility = Visibility.Visible;
}
}
}
Your problem comes at this line:
var addStep = doc.XPathSelectElement("Content/proceduralStep").Attribute(stepNumber);
It looks like you want to select the <proceduralStep> whose id attribute has value stepNumber. To do this with XPath, you need to use the syntax [#attributeName='attributeValue'], i.e.:
var addStep = doc.XPathSelectElement(string.Format("Content/proceduralStep[#id='{0}']", stepNumber));
Note that, if stepNumber were a user-entered string, you would need to be careful to prevent XPath injection, for instance by following the instructions here.
Alternatively, you could do this using by appending a Where expression:
var addStep = doc.XPathSelectElements("Content/proceduralStep")
.Where(e => (string)e.Attribute("id") == stepNumber)
.FirstOrDefault();
XPath injection is not an issue with this approach.
(The method you are currently calling, XElement.Attribute(XName), returns the attribute with the specified name, which you do not want.)
Then add() needs to be correctly capitalized as XElement.Add():
addStep.Add(
new XElement("proceduralStep",
new XAttribute("id", stepNumber + "-" + counter2.ToString("D4")),
new XElement("para", subDataBox.Text)));
Related
I have a dropdown list on a webform. It contains state names. The dropdown list is populated using an xml file at page load. When I select a specific state I have the onselectedindexchange event firing. When I select Florida which is index 9, the index changes to 7 which is conneticut. I am lost as to what is going on. Any help would be appreciated. I am not a professional programmer. Thanks
Here is the xml file data.
<state name="Delaware" abs="DE">
<id>8</id>
<counties>
<county id="1">Kent</county>
<county id="2">New Castle</county>
<county id="3">Sussex</county>
</counties>
</state>
<state name="Florida" abs="FL">
<id>9</id>
<counties>
<county id="1">Alachua</county>
<county id="2">Baker</county>
<county id="3">Bay</county>
<county id="4">Bradford</county>
<county id="5">Brevard</county>
<county id="6">Broward</county>
webpage control
<asp:DropDownList ID="ddlState" CssClass="form-control" runat="server" OnSelectedIndexChanged="ddlState_SelectedIndexChanged" AutoPostBack="true" TabIndex="0"></asp:DropDownList>
codebehind
I am getting the index and then filling another listbox control with the counties for the state collected
protected void ddlState_SelectedIndexChanged(object sender, EventArgs e)
{
int nodeid = ddlState.SelectedIndex;
ddStateComm.SelectedIndex = nodeid - 1;
countyfill(nodeid, sender);
countyfill(ddStateComm.SelectedIndex + 1, ddStateComm);
txtZipcode.Focus();
}
This is the code to get the county nodes for the state selected
protected void countyfill(int id, object sender)
{
XmlDocument doc = new XmlDocument();
doc.Load(MapPath("\~/App_Data/counties.xml"));
XmlNodeList ing = doc.SelectNodes("states/state[id = " + id + "]/counties/*");
if (sender.Equals(ddStateComm))
{
lstCounties.Items.Clear();
for (int i = 0; i < ing.Count; i++)
{
lstCounties.Items.Add(new ListItem(ing.Item(i).InnerText));
}
}
else
{
dpdCounty.Items.Clear();
for (int i = 0; i < ing.Count; i++)
{
dpdCounty.Items.Add(new ListItem(ing.Item(i).InnerText));
}
}
}
I tried to move the states at indexes 8 and 9 to 51 and 52 to see if a change would happen. All other states work fine. I have checked the xml file just to make sure there in not some simple missing end tag causing the problem with those specific states but I don't see anything.
The cause of this error was caused by an incorrectly formed XML file. The attribute abr for abbreviation of state was misspelled on a few states as abs. So in the code above you will notice the attribute abs="FL" which is fine if they were all that way. All of the other states are using abr="State abbreviation" I hope this helps everyone. I should of had an XML Schema in place.
This is one of the tutorials on Microsoft about data binding with XML. I am using the source code provided on the web page.
In the application. It creates an XML in Window.Resources and data binding with other controls.
Xaml
<Window.Resources>
<!-- Books provider and inline data -->
<ObjectDataProvider x:Key="LoadedBooks" ObjectType="{x:Type linq:XElement}" MethodName="Parse">
<ObjectDataProvider.MethodParameters>
<system:String xml:space="preserve">
<![CDATA[
<books xmlns="http://www.mybooks.com">
<book id="0">book zero</book>
<book id="1">book one</book>
<book id="2">book two</book>
<book id="3">book three</book>
</books>
]]>
</system:String>
<linq:LoadOptions>PreserveWhitespace</linq:LoadOptions>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
...
</Window.Resouces>
// Other UI data binding code...
<StackPanel> ....
The XAML runs perfectly. Control can display the XML data correctly. However, in the code behind, it should load the XML data as XElement. So I can use a button click event to add/remove node.
CS
public partial class L2XDBForm : Window
{
XNamespace mybooks = "http://www.mybooks.com";
XElement bookList;
public L2XDBForm()
{
InitializeComponent();
// load xml data
bookList = (XElement) this.FindResource("LoadedBooks");
}
void OnAddBook(object sender, EventArgs e)
{
if (String.IsNullOrEmpty(tbAddID.Text) ||
String.IsNullOrEmpty(tbAddValue.Text))
{
MessageBox.Show("Please supply both a Book ID and a Value!", "Entry Error!");
return;
}
XElement newBook = new XElement(
mybooks + "book",
new XAttribute("id", tbAddID.Text),
tbAddValue.Text);
bookList.Add(" ", newBook, "\r\n");
}
// other event handler...
However, it shows System.InvalidCastException:“Unable to cast object of type 'System.Windows.Data.ObjectDataProvider' to type 'System.Xml.Linq.XElement'.”
It seems I cannot get Window.Resources xml object from code bookList = (XElement) this.FindResource("LoadedBooks");
And move on to the Remove button to delete node.
Remove Node from XML
void OnRemoveBook(object sender, EventArgs e)
{
int index = lbBooks.SelectedIndex;
if (index < 0) return;
XElement selBook = (XElement)lbBooks.SelectedItem;
//Get next node before removing element.
XNode nextNode = selBook.NextNode;
selBook.Remove();
//Remove any matching newline node.
if (nextNode != null && nextNode.ToString().Trim().Equals(""))
{ nextNode.Remove(); }
//Set selected item.
if (lbBooks.Items.Count > 0)
{ lbBooks.SelectedItem = lbBooks.Items[index > 0 ? index - 1 : 0]; }
}
This code works well. I can transfer a selected item as an XElement. So my question is, is there a better way to add the node or access the XML from Window.Resource? Should I transfer all the items in Listbox to an XElement object?
Thanks!
Sorry if this has been posted before, but all the solutions i could find only addressed XML files with on layer of children.
I have a form displaying a treeview. Instead of having to edit directly in the code, i want to create an XML file, and populate my treeview from that. My XML is similar to this:
<Root>
<Element>
<ChildElement>
<GrandChildElement></GrandChildElement>
<GrandChildElement></GrandChildElement>
</ChildElement>
<ChildElement>
<GrandChildElement></GrandChildElement>
<GrandChildElement></GrandChildElement>
</ChildElement>
</Element>
<Element>
<ChildElement>
<GrandChildElement></GrandChildElement>
<GrandChildElement></GrandChildElement>
</ChildElement>
<ChildElement>
<GrandChildElement></GrandChildElement>
<GrandChildElement></GrandChildElement>
</ChildElement>
</Element>
</Root>
The GrandChildElements, is the clickable nodes in my treeview, which can be used to select different things. The rest is used to visually categorize them.
So far i haven't managed to find a way to include the GrandChildElements. Populating it without the GrandChildElements, can be done through recursion like this:
private void treeView_Load(object sender, EventArgs e)
{
XmlDocument xmldoc = new XmlDocument();
System.IO.FileStream fs = new System.IO.FileStream("treeNodes.xml", FileMode.Open, FileAccess.Read);
xmldoc.Load(fs);
XmlNode xmlnode = xmldoc.ChildNodes[1];
tvKortskab.Nodes.Clear();
tvKortskab.Nodes.Add(new TreeNode(xmldoc.DocumentElement.Name));
TreeNode tNode = tvKortskab.Nodes[0];
AddNode(xmlnode, tNode);
}
private void AddNode(XmlNode inXmlNode, TreeNode inTreeNode)
{
XmlNode xNode;
TreeNode tNode;
XmlNodeList childNodes;
XmlNodeList subChildNodes;
if (inXmlNode.HasChildNodes)
{
childNodes = inXmlNode.ChildNodes;
for (int i = 0; i <= childNodes.Count - 1; i++)
{
xNode = childNodes[i];
inTreeNode.Nodes.Add(new TreeNode(xNode.Name));
tNode = inTreeNode.Nodes[i];
AddNode(xNode, tNode);
}
else
{
inTreeNode.Text = inXmlNode.InnerText.ToString();
}
}
I have tried nesting a loop inside that, checking if the childNodes had children, and then adding those. That worked, but also added a bunch of empty and duplicate nodes.
Your idea was correct and you did the right thing.
But at the else statement you did something wrong.
You say, that the "GrandChildElement" should show the InnerText and this text is empty, so you get an empty element. You need to use the Name property instead.
if (inXmlNode.HasChildNodes)
{
childNodes = inXmlNode.ChildNodes;
for (int i = 0; i <= childNodes.Count - 1; i++)
{
xNode = childNodes[i];
inTreeNode.Nodes.Add(new TreeNode(xNode.Name));
tNode = inTreeNode.Nodes[i];
AddNode(xNode, tNode);
}
}
else
{
inTreeNode.Text = inXmlNode.Name;
}
I hope I understand what you meant.
Since your GrandChildren nodes are empty,following line would show an empty value there.
inTreeNode.Text = inXmlNode.InnerText.ToString();
One possible solution is to show the node name if innerText is empty.
inTreeNode.Text = string.IsNullOrEmpty(inXmlNode.InnerText)? inXmlNode.Name : inXmlNode.InnerText.ToString();
PS: Btw, you had a possible typo here
XmlNode xmlnode = xmldoc.ChildNodes[1];
This needs to be
XmlNode xmlnode = xmldoc.ChildNodes[0];
Earlier Output
New Output
I'm trying to read an xml file and I want to do this:
A ComboBoxwhich will show all the vegetable names in the xml.
After selecting a vegetable, the second ComboBox will show the recipe names in the xml that could use the vegetable selected in the first ComboBox for cooking.
Last, with a OK button, the selected recipe will read the file path which leads to the recipe.
XML I wrote
<Vegetables>
<vegetable name="Carrot">
<recipe name="ABCrecipe">
<FilePath>C:\\</FilePath>
</recipe>
<recipe name="DEFrecipe">
<FilePath>D:\\</FilePath>
</recipe>
</vegetable>
<vegetable name="Potato">
<recipe name="CBArecipe">
<FilePath>E:\\</FilePath>
</recipe>
<recipe name"FEDrecipe">
<FilePath>F:\\</FilePath>
</recipe>
</vegetable>
</Vegetables>
C# code
public Form1()
{
InitializeComponent();
xDoc.Load("Recipe_List.xml");
}
XmlDocument xDoc = new XmlDocument();
private void Form1_Load(object sender, EventArgs e)
{
XmlNodeList vegetables = xDoc.GetElementsByTagName("Vegetable");
for (int i = 0; i < vegetables.Count; i++)
{
comboBox1.Items.Add(vegetables[i].Attributes["name"].InnerText);
}
}
private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
//I'm lost at this place.
}
The first ComboBox is now able to display the vegetable names, but how do I make the 2nd ComboBox to read the recipes according to the xml file?
You can build the following Xpath and then get the recipe for the vegetable
string xpath = string.Format("//vegetable[#name='{0}']/recipe",comboboxSelectedItem);
var selectedVegetableRecipe = xdoc.SelectSingleNode(xpath);
However, as Ondrej Tucny pointed out, during the application start you can cache the xml document in a static XMLDocument and then use it the code to avoid performance overhead for each call.
First of all, you aren't storing the parsed XML anywhere. Hence in comboBox1_SelectedIndexChanged you can't work with it. You should introduce a private field (or property, whatever) in your form instead of the xDoc local variable.
If you for some odd reason wanted to keep going the way you are working with the XML file right now, you would have to lookup the selected <vegetable> element in comboBox1_SelectedIndexChanged and then process all its child <recipe> elements. However, this is unnecessarily complicated. Better way is to start with declaring the data structure and use XML serialization.
You will end up with two classes, Vegetable and Recipe, and use XmlSerializer to serialize (store into XML) and deserialize (read from XML) you data. In the form you will then work with objects and don't have to play with XML manually.
Bassicly i can load my xml file correctly into a listview but i then want the user to be able to click on the items in the listview and from their display information about that item thats not in the listview but the xml.
So i thought i could load up an xml when the user clicks on the item and somehow fetch that information but it does not display anything on the label. Any help would be appreciated thanx.
XML:
<?xml version = "1.0" encoding="utf-8"?>
<project>
<test>
<code>ss</code>
<name>test</name>
</test>
<test>
<code>ss1</code>
<name>test1</name>
</test>
</project>
code:
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Xml;
using System.Xml.Linq;
namespace form
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
XmlTextReader reader = new XmlTextReader("XMLfile1.xml");
XmlNodeType type;
while (reader.Read())
{
type = reader.NodeType;
if (type == XmlNodeType.Element)
{
if (reader.Name == "name")
{
reader.Read();
listView1.Items.Add(reader.Value);
}
}
}
reader.Close();
}
private void Form1_Load(object sender, EventArgs e)
{
}
private void listView1_SelectedIndexChanged(object sender, EventArgs e)
{
XmlTextReader reader = new XmlTextReader("XMLFile1.xml");
XmlNodeType type;
while (reader.Read())
{
type = reader.NodeType;
if (type == XmlNodeType.Element)
{
if (reader.Name == "test")
{
reader.Read();
codelabel.Text = "s";
}
}
}
}
private void groupBox2_Enter(object sender, EventArgs e)
{
}
}
}
Using these extensions,
Write:
protected string GetName(string code)
{
var item = XElement.Load("XMLfile1.xml").GetEnumerable("test", x =>
new
{
Code = x.Get("code", string.Empty),
Name = x.Get("name", string.Empty)
})
.FirstOrDefault(i => i.Code == code);
if(null != item)
return item.Name;
return "Item not found";
}
This looks up the list from the file, check's each one until it finds the right code and then returns the name value. Then place that value in your control. If I have it wrong that you are searching by name, just exchange the code in the FirstOrDefault line with the return item.Name line.
Create a class that mimics the data in your file. Like:
public class Project
{
public string Code { get; set; }
public string Name { get; set; }
}
Then, you'll read your XML file into what will end up being a List. Take a look at this tutorial:
Simple XML Parsing
private List<Project> _projects;
You would then add your data from the List to your ListView.
foreach(Project proj in _projects)
{
listView1.Items.Add(proj.Name);
}
In your ListView SelectedIndexChanged you can access the proper index of the List to display the proper value to your label.
codeLabel.Text = _projects[listView1.FocusedItem.Index].Code;
The Value property for the test node is empty (it contains only sub-nodes, no text).
I think you should change your code to accumulate the text (with declarations, you can see an example on MSDN) from beginning of test until you read its end tag.
As alternative (if the XML file is not big) you may consider to use LINQ to XML or simply an XmlDocument (it's much more easy to read and you can always have InnerXml of any node). For an example you can look this article on CodeProject.
For a short list of what you can do to parse XML you can read this article (I don't think it's very good but at least it is a...list).