.ReplaceWith() not replacing XElement - c#

Within an xml document I have the following
...
<TablixMembers>
<TablixMember>
<Group Name="Details" />
</TablixMember>
</TablixMembers>
</TablixMember>
<TablixHeader Name="MyField3" />
<TablixHeader Name="MyField2" />
</TablixMembers>
</TablixMember>
I have a for loop that looks for each element with a field name ("MyField3, MyField2, ...)
for (int g = 0; g < remainingtotals; g++)
{
groupTotals.Reverse();
ff.Add(new XElement(ns + "TablixHeader"));
ff.Descendants(ns + "TablixHeader").Last().SetAttributeValue("Name", groupTotals[g]);
string nName = GenerateUniqueName("Nsted");
while (ff.Descendants(ns + "TablixHeader").Any(n => (string)n.Attribute("Name") == groupTotals[g]))
{
var newnode = ff.Descendants(ns + "TablixHeader").Where(n => (string)n.Attribute("Name") == groupTotals[g]).Single();
newnode.ReplaceWith(new XElement(tGroupHeader));
}
groupTotals.Reverse();
}
Within this loop I have a List<string> groupTotals which holds the names of the fields.
It may not need to be in a while loop (I added this for testing) I have a query that looks for the specific field
var newnode = ff.Descendants(ns + "TablixHeader").Where(n => (string)n.Attribute("Name") == groupTotals[g]).Single();
newnode.ReplaceWith(new XElement(tGroupHeader));
This sets newnode equal to:
<TablixHeader Name="MyField3" xmlns="http://schemas.microsoft.com/sqlserver/reporting/2010/01/reportdefinition" />
On the next line I am trying to replace this node with a new xml element structure
newnode.ReplaceWith(new XElement(tGroupHeader));
tGroupHeader is a memorystream that contains a large xml Element template that should be inserted and replace the current tablix node of the selected attribute name.
When this code runs the node does not get replaced. In fact nothing happens, I've set break points on the node, no xml exception is thrown it just steps right through it. I assume I am either calling the method incorrectly or the selecting of a single node is what is causing it to not replace the element.
I also thought it may be due to the injection of a large xml structure but I have also tested this using just a single element such as:
newnode.ReplaceWith(new XElement(ns + "Size"));// ns is the xml namespace for the schema I am using
this also did not result in the element being replaced.
Can anyone provide some insight into what I may be doing wrong?
-cheers

I finally found a work around for this it may not be the best approach but it was reliable. From what I could see when I attempted to query the node the element would be located but it would not update I think this was due in part to my attempt to inject and update based upon a .Descendants() query all within one method (this may not be right but it was all I could determine).
The work around approach that I used was instead of trying to navigate the xml structure for an element at the time of creating the element I added an ID attribute with a specific name.
ff.Add(new XElement(ns + "TablixMember"));
ff.Descendants(ns + "TablixMember").Last().SetAttributeValue("Name", groupTotals[g]);
ff.Descendants(ns + "TablixMember").Last().SetAttributeValue("ID", groupedItemTotals[g].GroupPosition.ToString());
This would then insert an XElement such as
<TablixMembers>
<TablixMember>
<TablixHeader xmlns="http://schemas.microsoft.com/sqlserver/reporting/2010/01/reportdefinition">
<Size>1in</Size>
<CellContents>
<Textbox Name="GroupTotalPPPccc2973356bf4beaa92cb0409b37b8ef" ID="1">
<CanGrow>true</CanGrow>
<KeepTogether>true</KeepTogether>
<--end snip -->
Once I added the id it made querying for the node much simpler
var newnode = ff.Descendants(ns + "TablixMember").Where(n => (string)n.Attribute("ID") == groupedItemTotals[g].GroupPosition.ToString()).Single();
From here I could then updated the elements within the node using Replace(). ReplaceWith() SetAttribut() etc.
in this snippet I selected the node, removed all child elements and then injected a new structure
newnode.RemoveAll();
newnode.Add(new XElement(tGroupHeader),
new XElement(ns + "KeepWithGroup"));
newnode.Descendants(ns + "TablixHeader").Last().Descendants(ns + "Textbox").Last().SetAttributeValue("Name", nName);
Finally, since the ID attribute was not supported by the current schema I was using I performed some clean up to remove any unsupported attributes.
report.Descendants(ns + "TablixMember").Where(p => (string)p.Attribute("ID") != null).Attributes("ID").Remove();
report.Descendants(ns + "Textbox").Where(p => (string)p.Attribute("ID") != null).Attributes("ID").Remove();
I'd be interested to see other suggestions or ideas.
thanks

Related

Can't loop children of returned XML Nodes in C#

I have a large, messy XML file and I want to retrieve ALL elements of the same name ("Item" for the sake of this post) from it, then be able to retrieve data from each element's children.
So far I have returned a list of every element called "Item" using this code, which just displays the namespace url and "Item" in p tags:
XDocument doc = XDocument.Load(#"C:\inetpub\wwwroot\mysite\myxml.xml");
XNamespace ns = "http://www.mynamespace.com";
var nodes = doc.Descendants().Elements(ns + "Item").Select(d => d.Name).ToList();
foreach(var x in nodes){
<p>#x</p>
}
However, by amending the code with the following, I can't retrieve any data of it's children and I get the error 'System.Xml.Linq.XName' does not contain a definition for 'Descendants':
foreach(var x in nodes){
<p>#x.Descendants().Element("Name")</p>
}
Here is a very basic version of my XML file:
<Item>
<Name>Item 1</Name>
<Type>Type 1</Type>
</Item>
I want to be able to search each 'Item' element for a 'Name' element and return the value. Can anyone see where I'm going wrong?
This is the problem:
.Select(d => d.Name)
You're explicitly selecting the names of the elements. If you want the actual elements (which I think you do), just get rid of that call:
var nodes = doc.Descendants().Elements(ns + "Item").ToList();
You could also get rid of the ToList() unless you need the query to be materialized eagerly.

processing Xhtml files with Xdocument class adds unwanted elements

I am working on a project which requires me to process xhtml files to fix the content of certain tags. The fixing itself is not a problem, however I have troubles when saving the files.
THe code I am using is:
var spanNodesList = p.GetSpanNodesList(xDoc);
foreach (XElement span in spanNodesList)
{
if (span.Value == null || span.Value == "")
{
span.Remove();
}
else
{
string[] words = p.SplitNodeText(span.Value);
XElement parent = span.Parent;
span.Remove();
foreach (string word in words)
{
parent.Add(new XElement("span", word,
new XAttribute("id", "w" + p.currentNodeID.ToString())));
p.currentNodeID++;
}
}
}
List<XElement> GetSpanNodesList(XDocument file)
{
//Get only 'word' nodes
var spanNodes = file.Descendants("{http://www.w3.org/1999/xhtml}span");
if (spanNodes != null)
{
var spanNodesList = spanNodes.ToList();
spanNodesList.RemoveAll(x => ((x.Attribute("id") == null) || !x.Attribute("id").Value.Contains("w")));
return spanNodesList;
}
else return null;
}
As firstly, I couldn't get any elements, I have found out somewhere in SO that I might need to add namespace reference to file.Descendants("{http://www.w3.org/1999/xhtml}span"); as it yielded no results. This has indeed helped and I get the nodes I want. However, the resulting code produces has two problems.
<span id="w1" xmlns="">Word one</span>
<span id="w2" xmlns="">Word two</span>
<span id="w3" xmlns="">Word three</span>
It adds the xmlns attribute, which I don't need (and which was not in the original file) and it adds <?xml version="1.0" encoding="utf-8"?> header. I assume this is expected behaviour resulting from what I coded, so my question is - what can I do to remove these 'problems'. Or perhaps there's a better way of dealing with xHtml files? Also, I don't know if this is relevant, but source files have references to a number of different namespaces...
Cheers
Bartosz
When you add the span element, you're doing it without a namespace - whereas some ancestor element has set the default namespace. All you need to do is use the right namespace for your new elements:
XNamespace ns = "http://www.w3.org/1999/xhtml";
...
parent.Add(new XElement(ns + "span", ...);
Likewise you can use:
var spanNodes = file.Descendants(ns + "span");
which is rather more readable, IMO. You almost certainly don't need to worry about the XML declaration.

XElement.Element returning null for newly created element

I am using XElement to create an XMLDocument which is used in a hierarchical WPF treeview. If I create a new element with :
x_element = new XElement("node",
new XElement("tree_id", strData[0]),
new XElement("sys_id", dpl.DepSysId),
new XElement("part_id", strData[8]),
new XElement("make", strData[6]),
new XElement("model", strData[5]),
new XElement("level", strData[2]));
I then need to add attributes to "node" so I tried:
XElement temp_el = x_element.Element("node"); // This is returning null
temp_el.SetAttributeValue("title", strData[7] + " " + strData[6] + " " + strData[5]);
temp_el.SetAttributeValue("canEdit", "False");
temp_el.SetAttributeValue("status", nStatus.ToString());
temp_el.SetAttributeValue("qty", strData[13]);
temp_el.SetAttributeValue("part", strData[8]);
In the above code temp_el is null, but I can see in the debugger that x_element contains the following :
<node>
<tree_id>82</tree_id>
<sys_id>82</sys_id>
<part_id>169</part_id>
<make>ST Panel</make>
<model>Logical Pure 16 tube Collector</model>
<level>0</level>
</node>
To work around this I have used the following:
foreach (XElement temp_el in x_element.DescendantsAndSelf())
{
if (temp_el.Name == "node")
{
temp_el.SetAttributeValue("title", strData[7] + " " + strData[6] + " " + strData[5]);
temp_el.SetAttributeValue("canEdit", "False");
temp_el.SetAttributeValue("status", nStatus.ToString());
temp_el.SetAttributeValue("qty", strData[13]);
temp_el.SetAttributeValue("part", strData[8]);
break;
}
}
Whilst the above works I am just curious as to why I am getting null returned. Is my workaround the best way of doing this?
Regards.
You defined your XElement like this:
x_element = new XElement("node", /* child nodes */);
Where "node" is the name of the XElement you are creating, and the following parameters are its children.
By using x_element.Node("node"), you are trying to get the child node named "node", and there isn't such a child node.
x_element itself is the node named "node".
DescendantsAndSelf worked because it includes x_element (hence "AndSelf"), but you don't need this either because you already have the node.
So you can change your second code snippet to this:
x_element.SetAttributeValue("title", strData[7] + " " + strData[6] + " " + strData[5]);
x_element.SetAttributeValue("canEdit", "False");
// etc.
(BTW, you can also add the Attributes in the constructor)
Because with your first temp_el,
XElement temp_el = x_element.Element("node");
You used to get nodes which is not treated to be an Element of x_element.
It was treated as its root. However, with the second one,
x_element.DescendantsAndSelf()`
You used this XElement Method which treat node itself as an element.
XContainer.Elements Method - Returns a collection of the child elements of this element or document, in document order.
XElement.DescendantsAndSelf Method - Returns a collection of elements that contain this element, and all descendant elements of this element, in document order.
To solve the issue I used Descendants(). Here is my code snippet
public void UpdateEnquiry([FromBody]XElement UpdatePurchaseOrder)
{
var obj = XElement.Parse(UpdatePurchaseOrder.ToString());
var ii = (from v in obj.Descendants() select new { v.Value }).ToList() ;
}

Getting the right node in Linq to XML

Im trying to parse an XML file containing all the uploaded videos on a certain channel. Im attempting to get tbe value of the URL attribute in one of the <media:content> nodes and put it in the ViewerLocation field. However there are several of them. My current code is this:
var videos = from xElem in xml.Descendants(atomNS + "entry")
select new YouTubeVideo()
{
Title = xElem.Element(atomNS + "title").Value,
Description = xElem.Element(atomNS + "content").Value,
DateUploaded = xElem.Element(atomNS + "published").Value,
ThumbnailLocation = xElem.Element(mediaNS + "group").Element(mediaNS + "content").Attribute("url").Value,
ViewerLocation = xElem.Element(mediaNS + "group").Element(mediaNS + "content").Attribute("url").Value
};
It gets me the first node in the XML for entry with the name <media:content> as you would expect. However, the first entry in the XML isn't what I want. I want the second.
Below is the relevant XML.
<!-- I currently get the value held in this node -->
<media:content
url='http://www.youtube.com/v/ZTUVgYoeN_b?f=gdata_standard...'
type='application/x-shockwave-flash' medium='video'
isDefault='true' expression='full' duration='215' yt:format='5'/>
<!-- What i actually want is this one -->
<media:content
url='rtsp://rtsp2.youtube.com/ChoLENy73bIAEQ1kgGDA==/0/0/0/video.3gp'
type='video/3gpp' medium='video'
expression='full' duration='215' yt:format='1'/>
<media:content
url='rtsp://rtsp2.youtube.com/ChoLENy73bIDRQ1kgGDA==/0/0/0/video.3gp'
type='video/3gpp' medium='video'
expression='full' duration='215' yt:format='6'/>
I want the second node because it has a type of 'video/3gpp'. How would I go about selecting that one? My logic would be
if attribute(type == "video/3gpp") get this value.
But i do not know how to express this in Linq.
Thanks,
Danny.
Probably something like;
where xElem.Element(atomNS + "content").Attribute("type").Value == "video/3gpp"
Edit: I didn't quite know how to expand and explain this one without assuming the OP had no knowledge of Linq. You want to make your original query;
from xElem in xml.Descendants(atomNS + "entry")
where xElem.Element(atomNS + "content").Attribute("type").Value == "video/3gpp"
select new YouTubeVideo() {
...
}
You can interrogate attributes of a node, just like you can look at the elements of the document. If there are multiple elements with that attribute, you could then (assuming you always want the first you find)..
( from xElem in xml.Descendants(atomNS + "entry")
where xElem.Element(atomNS + "content").Attribute("type").Value == "video/3gpp"
select new YouTubeVideo() {
...
}).First();
I changed the original post, as I believe the node you're querying is the Element(atomNS + "content"), not the top level xElem
Using XPath from this Xml Library (Just because I know how to use it) with associated Get methods:
string videoType = "video/3gpp";
XElement root = XElement.Load(file); // or .Parse(xmlstring)
var videos = root.XPath("//entry")
.Select(xElem => new YouTubeVideo()
{
Title = xElem.Get("title", "title"),
Description = xElem.Get("content", "content"),
DateUploaded = xElem.Get("published", "published"),
ThumbnailLocation = xElem.XGetElement("group/content[#type={0}]/url", "url", videoType),
ViewerLocation = xElem.XGetElement("group/content[#type={0}]/url", "url", videoType)
});
If the video type doesn't change, you can replace the XGetElement's with:
xElem.XGetElement("group/content[#type='video/3gpp']/url", "url")
Its a lot cleaner not having to specify namespaces using the library. There is the Microsoft's XPathSelectElements() and XPathSelectElement() you can look into, but they require you to specify the namespaces and don't have the nice Get methods imo. The caveat is that the library isn't a complete XPath implementation, but it does work with the above.

C# Using XPATH to select specific element with known values, then delete

I am new to xpath in C# and I am trying to select an element that has two specific values. This is what the XML format looks like
<?xml version="1.0" encoding="utf-8"?>
<Manager>
<SSH>
<Tunnels>
<Port>
<Local>443</Local>
<Remote>443</Remote>
</Port>
<Port>
<Local>5432</Local>
<Remote>5432</Remote>
</Port>
<Port>
<Local>19</Local>
<Remote>21</Remote>
</Port>
<Port>
<Local>19</Local>
<Remote>22</Remote>
</Port>
</Tunnels>
</SSH>
</Manager>
I was trying to select a 'Port' that had the values from a previous form so i can delete that specific entry from the xml. This was the code i was using:
//remove children from selected
XmlNode _xmlTunnel = _xml.SelectSingleNode("/Manager/SSH/Tunnels/Port[Local=" + _local + "] | /Manager/SSH/Tunnels/Port[Remote=" + _remote + "]");
MessageBox.Show("Local " + sshList.SelectedItems[0].Text + " Remote " + sshList.SelectedItems[0].SubItems[1].Text +"\n\n" + _xmlTunnel.InnerText);
_xmlTunnel.RemoveAll();
//remove all empties
XmlNodeList emptyElements = _xml.SelectNodes(#"//*[not(node())]");
for (int i = emptyElements.Count -1; i >= 0; i--) {
emptyElements[ i ].ParentNode.RemoveChild(emptyElements[ i ]); }
This code works fine until I have two Ports with the same Local Value. It always selects the first element that it comes to (i.e. Local=19 and Remote=21, even if you try to select the node where Local=19 and Remote=22). I tried switching the xpath expression to 'and' instead of '|' in the SelectSingleNode method but that errors out with a "Expression must evaluate to a node-set". Which makes me think that I am evaluating out to a boolean when I use 'and'.
Is the better way to do this by a loop where a select the first element and loop until the second one matches? As I said before i do not have much experience with xpath/xml expressions in C#, perhaps there is a better way. If it helps I am using windows forms and .net 4.0, in this form the port values fill a two column list view in detail view.
You need to "and" 2 conditions on Port node like:
"/Manager/SSH/Tunnels/Port[Local=" + _local + " and Remote=" + _remote + "]"
In your case you are doing union of 2 sets where Local=19 and another where Remote=21.

Categories

Resources