Deborah's Developer MindScape






         Tips and Techniques for Web and .NET developers.

Archive for XML

March 25, 2010

Finding a set of Nodes in an XML String

Filed under: C#,LINQ,VB.NET,XML @ 11:48 am

In this prior post, I demonstrated how to find a node in an XML string. In this post, I expand on that topic to find a set of nodes. You can then process those nodes as needed in your application. In this example, the set of nodes are displayed in a ComboBox.

If you are targeting the .NET Framework version 3.5 or later, you can use Linq to XML to retrieve a set of nodes. This example using Linq.

Here is the XML we will use in this example:

<States>
  <State name="California">
    <Regions>
      <Region name="San Luis Obispo">
      <Area name="Santa Maria" />
      <Area name="Seaside" />
      </Region>
      <Region name="Silicon Valley">
        <Area name="San Jose"/>
        <Area name="Sunnyvale"/>
      </Region>
      <Region name="Springfield">
        <Area name="Emeryville"/>
        <Area name="Hooterville"/>
      </Region>
    </Regions>
  </State>
  <State name="Wisconsin">
    <Regions>
      <Region name="Milwaukee">
        <Area name="Mukwanago"/>
        <Area name="Germantown"/>
      </Region>
      <Region name="Fox Valley">
        <Area name="Oshkosh" />
        <Area name="Appleton" />
      </Region>
      <Region name="Springfield">
        <Area name="Ogdenville"/>
        <Area name="Shelbyville"/>
      </Region>
    </Regions>
  </State>
</States>

Starting with a simple example, let’s find the set of all regions and display them in a combo box.

In C#:

// Build a list of region names
var regionElements = doc.Descendants("Region");
var regionEnumeration = regionElements.Select
                        (r => r.Attribute("name").Value);
List<string> regionList = regionEnumeration.ToList();
regionList.Sort();

// Bind to a ComboBox
comboBox1.DataSource = regionList;

// OR

List<string> regions = doc.Descendants("Region").Select
                       (r => r.Attribute("name").Value).ToList();
regions.Sort();
comboBox1.DataSource = regions;

In VB:

‘ Build a list of region names
Dim regionElements = doc…<Region>
Dim regionEnumeration = regionElements.Select(Function(r) r.@name)
Dim regionList As List(Of String) = regionEnumeration.ToList
regionList.Sort()

‘ Bind to a ComboBox
ComboBox1.DataSource = regionList

‘ OR

Dim regions As List(Of String) = doc…<Region>.Select _
                                 (Function(r) r.@name).ToList
regions.Sort()
ComboBox1.DataSource = regions

The code first finds the set of region elements. The C# code uses the XElement methods and the VB code uses XML literals.

Only the name of the regions are needed, so the next statement selects the name attribute. Again, the C# code uses the XElement methods and the VB code uses XML literals.

NOTE: You can also use the XElement methods in VB as well.

The enumeration is then converted to a list so it can be sorted and assigned as a DataSource.

The second set of code combines the set of Linq to XML statements into one for a more condensed version of the code.

The result is as follows:

image

Use this technique any time you need to retrieve a set of nodes from an XML file.

Enjoy.

February 19, 2010

XML Literals: Escaping Characters

Filed under: VB.NET,XML @ 8:14 pm

In XML, there are several characters that have special meaning, such as the less than (<), greater than (>) and quotation mark ("). If you just type these characters in to your XML literal, your application won’t understand them.

NOTE: All of the code in this post is in Visual Basic since C# does not directly support XML literals.

For example, the following code will not compile. It tries to interpret the less than and greater than signs as XML elements.

In VB:

Dim myXML As XElement = <Product>
                           <Description> 
                      This is a product that has special characters: 
                      Ampersand: &, Apostrophe: ‘, Quote: " 
                      Less than: <, Greater than: >"
                           </Description>
                        </Product>

To get this code to compile, replace the problematic symbols with HTML character entities:

  • < becomes &lt;
  • > becomes &gt;
  • & becomes &amp;

The resulting code is as follows:

Dim myXML As XElement = <Product>
                           <Description> 
                      This is a product that has special characters: 
                      Ampersand: &amp;, Apostrophe: ‘, Quote: " 
                      Less than: &lt;, Greater than: &gt;"
                           </Description>
                        </Product>

This works, but is not very nice to look at, especially if you are not familiar with HTML character entities.

Another option is to use the RegularExpression Escape and Unescape methods.

NOTE: Be sure to import the System.Text.RegularExpression namespace.

In VB:

Dim s As String= "This is a product that has special characters: " & _
                    Environment.NewLine & _
                    "Ampersand: &, Apostrophe: ‘, Quote: "" " & _
                    Environment.NewLine & _
                    "Less than: <, Greater than: >"

Dim myXML As XElement = <Product>
                            <Description>
                                <%= Regex.Escape(s) %>
                            </Description>
                        </Product>
Dim description As String = Regex.Unescape(myXML.<Description>.Value)

MessageBox.Show(description)

This code results in the following MessageBox:

clip_image002

The only character that you still need to escape is the quotation mark. You can escape it in the string by doubling it ("").

The RegEx.Escape takes care of escaping the necessary characters for the XML string. The RegEx.Unescape takes care of unescaping the characters for viewing.

Enjoy!

January 31, 2010

Finding a Node in an XML String

Filed under: C#,Lambda Expressions,LINQ,VB.NET,XML @ 4:21 pm

A common requirement with an XML file is to find a particular node. If you are targeting the .NET framework 3.5 or higher, you can find the node using Linq to XML or by using Lambda expressions.

As with many of my prior XML examples, the XML string is as follows:

<States>
  <State name="Wisconsin">
    <Regions>
      <Region name="Milwaukee">
        <Area name="Mukwanago"/>
        <Area name="Germantown"/>
      </Region>
      <Region name="Fox Valley">
        <Area name="Oshkosh" />
        <Area name="Appleton" />
      </Region>    
    </Regions>
  </State>
</States>

The code to find the node for the Milwaukee region is as follows:

In C#:

// Be sure to set a reference to System.Core and System.Xml.Linq
XElement states  = XElement.Load("testXML.xml");

// Using LINQ
XElement foundNode;
var query = from XElement r in states.Descendants("Region")
                   where r.Attribute("name").Value == "Milwaukee"
                   select r;
foundNode = query.FirstOrDefault();

// Using Lambda expressions
foundNode = states.Descendants("Region").
     Where(r => r.Attribute("name").Value ==
                         "Milwaukee").FirstOrDefault();

In VB:

‘ Be sure to set a reference to System.Core and System.Xml.Linq
Dim states As XElement = XElement.Load("testXML.xml")

‘ Using LINQ
Dim foundNode As XElement
Dim query = From r As XElement In states…<Region> _
                  Where r.@<name> = "Milwaukee"
foundNode = query.FirstOrDefault()

‘ Using Lambda expression
foundNode = states…<Region>.Where(Function(r) r.@<name> =  _ 
                                 "Milwaukee").FirstOrDefault

This code first loads the XML file containing the XML. The next set of code can be done using LINQ or using Lambda expressions. Use either one, but not both. πŸ™‚

The C# code uses the XElement properties and methods. The VB code uses XML literals.

NOTE: The XElement properties and methods work in VB as well.

Enjoy!

NOTE: This post was created based on a prior post that included both finding a node and adding new nodes. This post separates the first step to provide a more straightforward example.

XML Documentation Comments

Filed under: C#,VB.NET,XML @ 2:37 pm

You can document your classes, properties, methods, and so on using XML tags. I’m sure all developers know this at this point, but did you know that you can modify the set of valid tags?

If you are not familiar with XML Documentation Comments (or just XML Comments), you create them differently depending on the language you are using.

In C#:

On the line above the class, property, method, or whatever you are documenting, type three forward slashes.

///
public List<Customer> Retrieve(int Id)

Visual Studio automatically inserts the appropriate XML tags:

/// <summary>
///
/// </summary>
/// <param name="Id"></param>
/// <returns></returns>
public List<Customer> Retrieve(int Id)

In VB:

On the line above the class, property, method, or whatever you are documenting, type three apostrophes.

”’
Public Function Retrive(ByVal id As Integer) As List(Of Customer)

Visual Studio automatically inserts the appropriate XML tags:

”’ <summary>
”’
”’ </summary>
”’ <param name="id"></param>
”’ <returns></returns>
”’ <remarks></remarks>
Public Function Retrive(ByVal id As Integer) As List(Of Customer)

You can then build technical documentation of your API from the XML comments.

In C#:

1. Double-click on Properties for the project in the Solution Explorer.

2. Click on the Build tab.

3. Check the "XML document file" checkbox and ensure  that a file name is specified.

4. Build your project.

All of the XML comments are generated into one XML file.

In VB:

Generating the XML documentation is on by default. To check it:

1. Double-click on My Project for the project in the Solution Explorer.

2. Click on the Compile tab.

3. Check the "Generate XML documentation file" checkbox.

The XML file is generated in the bin\Debug folder for the project.

You can use a product such as SandCastle to generate your technical documentation from these comments.

But what if you want more or different tags than those that are provided? IF YOU ARE USING VB.NET you can do just that.

NOTE: As far as I have seen, this technique is not available in C#.

In VB:

Locate your XML Comment tag file. Mine was here:

C:\Documents and Settings\Deborah\Application Data\Microsoft\VisualStudio\9.0\VBXMLDoc.xml

If you don’t have this file, you can create it following the instructions defined here.

This file contains the definition of the valid XML tags used in XML Comments. You can then add to the contents of this file or edit it as you desire.

For one of my projects, we wanted to add an edit history to each class that included the date, the developer’s name, and a description. So I added the following to the Class code element:

    <CodeElement type="Class">
        <Template>
            <summary/>
            <remarks/>
        <editHistory date="" developer=""/>
        </Template>
        <CompletionList>
            <include file="" path=""/>
            <permission cref=""/>
            <remarks/>
            <summary/>
        <editHistory date="" developer=""/>
        </CompletionList>
    </CodeElement>

We could then create XML comments as follows:

”’ <summary>
”’ Manages a customer class
”’ </summary>
”’ <remarks></remarks>
”’ <editHistory  date="9/14/09" developer="DJK">
”’ Added customer type.
”’ </editHistory>
”’ <editHistory date="10/17/09" developer="DJK">
”’ Added an Email address.
”’ </editHistory>
Public Class Customer

Use this technique any time you want to enhance your XML comment documentation in VB.NET.

Enjoy!

October 20, 2009

Populating a DataGridView from Xml Data

Filed under: C#,Data Binding,LINQ,VB.NET,WinForms,XML @ 11:49 pm

If you are using XML in a WinForms application you may find the need to display the XML data in a DataGridView.

Let’s take this XML:

<states>
    <state name="California">
        <abbreviation>CA</abbreviation>
        <year>1850</year>
        <governor>Schwarzenegger</governor>
    </state>
    <state name="Wisconsin">
        <abbreviation>WI</abbreviation>
        <year>1848</year>
        <governor>Doyle</governor>
    </state>
</states>

Displaying XML in a DataGridView sounds easy, but if you just set the DataGridView DataSource to the XML data, you will get something like this:

image

Notice how it has lots of yuck in it: attribute details, node properties, and so on for every node in the XML. So how do you get something more like this:

image

The trick is to use anonymous types.

The code is provided here in VB and C# and then described in detail below.

NOTE: Be sure to set a reference to System.Core and System.Xml.Linq

In C#:

XElement statesXml = XElement.Parse("<states>" +
    "<state name=’California’>" +
        "<abbreviation>CA</abbreviation>" +
        "<year>1850</year>" +
        "<governor>Schwarzenegger</governor>" +
    "</state>" +
    "<state name=’Wisconsin’>" +
        "<abbreviation>WI</abbreviation>" +
        "<year>1848</year>" +
        "<governor>Doyle</governor>" +
    "</state>" +
   "</states>");

var query = from st in statesXml.Descendants("state")
            select new
            {
                Name = st.Attribute("name").Value,
                Abbrev = st.Element("abbreviation").Value,
                Year = st.Element("year").Value,
                Governor = st.Element("governor").Value
            };
DataGridView1.DataSource = query.ToList();

In VB:

Dim statesXml As XElement = _
    <states>
        <state name="California">
            <abbreviation>CA</abbreviation>
            <year>1850</year>
            <governor>Schwarzenegger</governor>
        </state>
        <state name="Wisconsin">
            <abbreviation>WI</abbreviation>
            <year>1848</year>
            <governor>Doyle</governor>
        </state>
    </states>

Dim query = From st In statesXml…<state> _
            Select New With { _
                  .Name = st.@name, _
                  .Abbrev = st.<abbreviation>.Value, _
                  .Year = st.<year>.Value, _
                  .Governor = st.<governor>.Value}
DataGridView1.DataSource = query.ToList

The first part of this code builds the XML. The C# code uses the XElement.Parse function to build the XML; VB uses XML literals. This part of the code is not necessary if you are reading the XML from another source, such as a file.

The second part of the code leverages Linq to XML to process the set of state XML elements. For each element, it uses the Select New syntax to create an anonymous type. The syntax defines an unnamed (anonymous) type with properties Name, Abbrev, Year, and Governor.

[To view an overview of anonymous types, start here.]

The last line converts the results of the query to a generic list and assigns it to the DataSource property of the DataGridView. Visual Studio uses the anonymous type property names as the text for the column titles and populates the rows with each state element.

Use this technique any time you have XML that you want to display in a DataGridView.

Enjoy!

August 25, 2009

Text Files

Filed under: C#,Text Files,VB.NET,XML @ 1:09 pm

There are many different types of text files that you may need to process in your applications. Some of the more common types are described in this post.

Delimited files

Delimited files separate the fields of the file with some type of a delimiter. The most common delimited files are comma separated value (CSV) files, as shown below.

1,  Baggins, Bilbo, 20090811
2,  Baggins, Frodo, 20090801
3,  Gamgee,  Samwise, 20090820
4,  Cotton,  Rosie, 20090821

Other field delimiters, such as tabs or semicolons, can also be used.

In most cases, delimited files define the end of each record with a CrLf (carriage return/linefeed). So the example above contains four records, one for each line of data.

If you need to read the data from a delimited file into your application, there are features in .NET to assist you.

You can read the file directly using the file classes within .NET and then process the columns using String functions.

Alternatively, you can read the file into a DataTable, even if you have no plans to use this data in a database. This allows you to work with the data as a set of rows and columns instead of using a large number of string manipulation functions. See this link for an example of reading a CSV text file into an in-memory DataTable.

Another option is to read the file using VB’s TextFieldParser class. See this link for an example.

Fixed length files

Fixed length files define data within specific columns. In the example below, the customer Id is left-justified in the first 8 columns, the last name is left-justified in the next 20 columns, the first name is left-justified in the next 10 columns, and the last edit date is left justified in the last 10 columns.

000001  Baggins             Bilbo     20090811
000002  Baggins             Frodo     20090801
000003  Gamgee              Samwise   20090820
000004  Cotton              Rosie     20090821

As with delimited files, the end of each record is normally defined with a CrLf (carriage return/linefeed). So the example above contains four records, one for each line of data.

This style of text file is often used by legacy systems, especially mainframe systems.

The StringBuilder class in the .NET framework provides easy to use formatting features that help you write code to output fixed length files. See this link for an example.

If you need to read the data from a delimited file into your application, there are features in .NET to assist you.

You can read the file directly using the file classes within .NET and then process the columns using String functions.

Alternatively, you can read the file into a DataTable, even if you have no plans to use this data in a database. This allows you to work with the data as a set of rows and columns instead of using a large number of string manipulation functions. See this link for an example of reading a fixed length file into an in-memory DataTable.

Another option is to read the file using VB’s TextFieldParser class. See this link for an example.

XML files

XML (eXtensible Markup Language) files follow the XML specification to define hierarchical data. You use XML elements, defined with start (< >) and end (</ >) tags and XML attributes, define with name/value pairs within a start element tag, to define the structure and content of a file.

For example, this XML file defines a set of customers, each delimited within customer tags. Each field defined for the customer is defined in an element within the customer open and close tags. In the example below, each customer has a CustomerId, LastName, FirstName, and LastUpdateData.

<customers>
  <customer>
    <CustomerId>1</CustomerId>
    <LastName>Baggins</LastName>
    <FirstName>Billbo</FirstName>
    <LastUpdateDate>20090811</LastUpdateDate>
  </customer>
  <customer>
    <CustomerId>2</CustomerId>
    <LastName>Baggins</LastName>
    <FirstName>Frodo</FirstName>
    <LastUpdateDate>20090801</LastUpdateDate>
  </customer>
  <customer>
    <CustomerId>3</CustomerId>
    <LastName>Gamgee</LastName>
    <FirstName>Samwise</FirstName>
    <LastUpdateDate>20090820</LastUpdateDate>
  </customer>
  <customer>
    <CustomerId>4</CustomerId>
    <LastName>Cotton</LastName>
    <FirstName>Rosie</FirstName>
    <LastUpdateDate>20090821</LastUpdateDate>
  </customer>
</customers>

If you have any choice on the type of text file to use for your application, an XML file is the best choice. XML has become the standard for defining data structures due to its simplicity, clarity, self-descriptiveness, and consistency. There are great tools for reading and writing XML files available in the .NET framework. Visual Basic has additional features, called XML literals, that make working with XML files a breeze. See this link for other posting on working with XML files.

Sometimes, however, you have no choice. You have to read a text file that is not in an XML structure. For example, you may need to read files generated by legacy systems or by other software. In that case, you need to use delimited or fixed length files.

August 20, 2009

Adding Nodes to an XML String

Filed under: C#,Lambda Expressions,LINQ,VB.NET,XML @ 5:49 pm

I have an XML string as follows:

<States>
  <State name="Wisconsin">
    <Regions>
      <Region name="Milwaukee">
        <Area name="Mukwanago"/>
        <Area name="Germantown"/>
      </Region>
      <Region name="Fox Valley">
        <Area name="Oshkosh" />
        <Area name="Appleton" />
      </Region>    
    </Regions>
  </State>
</States>

I now want to add another area under Milwaukee called "Wauwatosa".

The code to accomplish this task is as follows.

In C#:

// Be sure to set a reference to System.Core and System.Xml.Linq
XElement states  = XElement.Load("testXML.xml");

// New element under the Milwaukee region
XElement newElement  = XElement.Parse(@"<Area name=’Wauwatosa’/>");

// Find the desired parent element
// Using LINQ
XElement parentNode;
var parentQuery = from XElement r in states.Descendants("Region")
                   where r.Attribute("name").Value == "Milwaukee"
                   select r;
parentNode = parentQuery.FirstOrDefault();

// Using Lambda expression
parentNode = states.Descendants("Region").
     Where(r => r.Attribute("name").Value ==
               
"Milwaukee").FirstOrDefault();

if (parentNode != null)
     parentNode.Add(newElement);

states.Save("Revised.xml");

In VB:

‘ Be sure to set a reference to System.Core and System.Xml.Linq
Dim states As XElement = XElement.Load("testXML.xml")

‘ New element under the Milwaukee region
Dim newElement As XElement = <Area name="Wauwatosa"/>

‘ Find the desired parent element
‘ Using LINQ
Dim parentNode As XElement
Dim parentQuery = From r As XElement In states…<Region> _
                  Where r.@<name> = "Milwaukee"
parentNode = parentQuery.FirstOrDefault()

‘ Using Lambda expression
parentNode = states…<Region>.Where(Function(r) r.@<name> =  _
                                    
"Milwaukee").FirstOrDefault

If (parentNode IsNot Nothing) Then
    parentNode.Add(newElement)
End If

states.Save("Revised.xml")

This code first loads in the XML file containing the XML at the top of this post. The code then defines the new element for Wauwatosa using XML literals in VB and the XElement properties and methods in C#.

NOTE: The XElement Descendants property works in VB as well.

The next set of code can be done using LINQ or using Lambda expressions. Use either one, but not both. πŸ™‚

If the appropriate parent element was found, the new element is added to it. The Add method adds the element as a child element.

Finally, the code saves the revised XML:

<States>
  <State name="Wisconsin">
    <Regions>
      <Region name="Milwaukee">
        <Area name="Mukwanago" />
        <Area name="Germantown" />
        <Area name="Wauwatosa" />
      </Region>
      <Region name="Fox Valley">
        <Area name="Oshkosh" />
        <Area name="Appleton" />
      </Region>
    </Regions>
  </State>
</States>

Enjoy!

July 21, 2009

Populating a TreeView Control from XML

Filed under: C#,VB.NET,WinForms,XML @ 4:58 pm

This post describes how to populate a WinForms TreeView control from an XML file assuming you are targeting the .NET Framework Version 3.5.

The XML file used in this example looks like this:

<States>
  <State name="California">
    <Regions>
      <Region name="San Luis Obispo">
        <Area name="Santa Maria" />
        <Area name="Seaside" />
      </Region>
      <Region name="Silicon Valley">
        <Area name="San Jose"/>
        <Area name="Sunnyvale"/>
      </Region>
    </Regions>
  </State>
  <State name="Wisconsin">
    <Regions>
      <Region name="Milwaukee">
        <Area name ="Mukwanago"/>
        <Area name="Germantown"/>
      </Region>
      <Region name="Fox Valley">
        <Area name="Oshkosh" />
        <Area name="Appleton" />
      </Region>    
    </Regions>
  </State>
</States>

The TreeView will display the following:

State
— Region
—— Area

The code is provided here in VB and C# and then described in detail below.

NOTE: Be sure to set a reference to System.Core and System.Xml.Linq

In C#:

XElement doc  = XElement.Load("testXML.xml");

TreeNode stateNode;
TreeNode regionNode;
foreach (XElement state  in doc.Descendants("State"))
{
    stateNode = treeView1.Nodes.Add(state.Attribute("name").Value);
    foreach (XElement region in state.Descendants("Region"))
    {
        regionNode =
            stateNode.Nodes.Add(region.Attribute("name").Value);
        foreach (XElement area in region.Descendants("Area"))
        {
            regionNode.Nodes.Add(area.Attribute("name").Value);
        }
    }
}

In VB:

Dim doc As XElement = XElement.Load("testXML.xml")

Dim stateNode As TreeNode
Dim regionNode As TreeNode
For Each state As XElement In doc…<State>
    stateNode = TreeView1.Nodes.Add(state.@name)
    For Each region As XElement In state…<Region>
        regionNode = stateNode.Nodes.Add(region.@name)
        For Each area As XElement In region…<Area>
            regionNode.Nodes.Add(area.@name)
        Next
    Next
Next

In both cases, the XML is first retrieved from a file. An in-memory XML string could be used instead.

Three for/each loops are used, one for each level of the TreeView hierarchy.

The states are processed first. A node is added for each state name.

The regions are processed next. A node is added under the state for each region name.

Finally, the areas are processed. A node is added under the region for each area name.

Notice that the VB code leverages XML literals. The C# code uses XElements (which also work in VB).

Enjoy!

July 8, 2009

XML Literals: Reading an XML File

Filed under: VB.NET,XML @ 4:28 pm

In a prior post here, I created an XML file using VB 9 (Visual Basic 2008/.NET Framework 3.5). This post demonstrates how to read that file and reconstitute the list of customers.

This code reads the XML into an XElement:

Dim customerXml As XElement = XElement.Load("customers.xml")

This code processes the XML and rebuilds the list of customers:

‘ Repopulate the business objects
Dim customerList as New List(Of Customer)
For Each c As XElement In customerXml…<customer>
    customerList.Add(New Customer With _
              
{.LastName = c.<LastName>.Value, _
                .FirstName = c.<FirstName>.Value})
Next

The first line of code defines the list that will contain the set of customer objects. The For/Each loop processes each <customer> element. For each element, a new Customer is created using the new object initializer feature in VB 9. The <LastName> and <FirstName> XML elements are read into the LastName and FirstName properties of the new customer. The new customer is then added to the list.

If all goes well, you should end up with the same list that we started with here.

This is almost too easy! πŸ™‚

Enjoy!

July 3, 2009

XML Literals: Simplifying Strings

Filed under: VB.NET,XML @ 12:51 pm

You have probably heard that the next version of VB will no longer require line continuation characters in most situations. This is very good news for those of us that do not like typing underscore characters.

But in the mean time, what do we do if we have a long string, like a SQL string:

Dim mySelect As String = "Select CustomerId, " & _
                            "LastName, " & _
                            "FirstName, " & _
                            "EmailAddress " & _
                            "From Customer " & _
                            "Where CustomerId = @CustomerID"

And this one is only selecting three fields!

XML Literals can help:

Dim mySelect2 As XElement = <string>
                            Select CustomerId,
                            LastName,
                            FirstName,
                            EmailAddress
                            From Customer
                            Where CustomerId = @CustomerId
                            </string>

Notice in the XML literals example that there are no quotes, ampersands (&) or underscores (_). That can save a lot of typing and it is easier to read.

Since mySelect2 is an XElement, use the Value property to get the string from the element.

mySelect2.Value

Enjoy!

« Previous PageNext Page »

© 2019 Deborah's Developer MindScape   Provided by WPMU DEV -The WordPress Experts   Hosted by Microsoft MVPs