Stumbling Through

Join me as I stumble, bumble and fumble my way through some new developer technologies. We'll laugh, we'll cry, there may be a mouse tossed through a monitor, but in the end we will all hopefully learn something.
in

Stumbling Through: LINQ

Technorati Tags: ,,

At the very beginning of my blogging days, I mentioned 'LINQ' as a technology that I was very interested in Stumbling Through... then I never mentioned it again.  Well, never fear all you 'Stumbling Through' fans (yes, both of you), I haven't forgotten this interesting bit of technology and have decided to work it into our People - Addresses project that we started up for Stumbling Through WPF (take a look at my post history for more information on this project).  Here is the plan: 

We currently have three collections defined -  People, Addresses and States.  Addresses are children of People, while States are kind of a lookup table.  What we'll do is load the records for these collections from xml files using LINQ, with the interesting caveat of including the full state name as a read-only property in the address object even though it resides in the States collection (the address object currently has only the state abbreviation).  Let's get started by creating two xml files:  People.xml and States.xml.  Here is the data for each:

 

People.xml

<?xml version="1.0" encoding="utf-8" ?>
<People>
  <Person FirstName="Test" LastName="Guy">
    <Address City="Chicago" State="IL" />
    <Address City="Mundelein" State="FL" />
    <Address City="Bartlett" State="CA" />
  </Person>
  <Person FirstName="Another" LastName="One">
    <Address City="Chicago" State="IL" />
    <Address City="Mundelein" State="FL" />
    <Address City="Lviv" State="CA" />
  </Person>
  <Person FirstName="Someone" LastName="Else">
    <Address City="Huntley" State="IL" />
    <Address City="Addison" State="IL" />
    <Address City="Bartlett" State="CA" />
  </Person>
</People>

 

 

States.xml

<?xml version="1.0" encoding="utf-8" ?>
<States>
  <State Name="Illinois" Abbreviated="IL"/>
  <State Name="Florida" Abbreviated="FL"/>
  <State Name="California" Abbreviated="CA"/>
</States>

 

 

We'll include these in our 'Stumbling Through' project under a 'Data' folder.  Make sure that each file has the 'Copy if New'flag set and Build Action is 'None', so it will be copied into a data folder relative to the executable (it'll make it easier to find the data at run-time). Now I want to modify the 'Load' methods of each collection to pull their data from these XML files instead of inserting hard-coded items.  We'll start with 'StateCollection.LoadAll', as it will probably be the simplest.  Here is the full code for the StateCollection class, I'll explain the 'LoadAll' method afterwards:

 

using System.Collections.ObjectModel;
using System.Linq;
using System.Xml.Linq;

namespace StumblingThroughWPFPartI
{
    public class StateCollection : ObservableCollection<State>
    {
        public void LoadAll()
        {
            this.Clear();

            XElement xmlElement = XElement.Load("Data/States.xml");

            foreach (State state in
                from xmlState in xmlElement.Descendants("State")
                select new State(xmlState.Attribute("Abbreviated").Value, xmlState.Attribute("Name").Value)
            )
            {
                this.Add(state);
            }
        }
    }
}

 

Wow, that is some crazy syntax in the 'LoadAll' method... what does it mean?  Firstly, we clear any items in the the list.  Then, we load up an XElement (Xml.Linq namespace) with the data in our external xml file.  We then iterate through each state identified in the xml file, instantiate a state object using the element's attribute values in the constructor, and add the state instance to our collection.

Something else interesting with the LINQ syntax is that if we wanted to populate our State object properties after instantiating it (that is, there was an empty constructor), we could substitute the following for our previous 'foreach' statement:

 

foreach (State state in
    from xmlState in xmlElement.Descendants("State")
    select new State()
    {
        Abbreviated = xmlState.Attribute("Abbreviated").Value,
        Name = xmlState.Attribute("Name").Value
    }
)

 

This instantiates the State object, and then sets its 'Abbreviation' and 'Name' properties all in the LINQ statement!

I am very impressed with LINQ so far, even if the syntax is a little confusing (why 'from' element 'in' source?  Just to avoid confusion with 'for'?).  Let's move on to our 'PersonCollection.LoadAll', which should be just as simple:

 

using System.Collections.ObjectModel;
using System.Linq;
using System.Xml.Linq;

namespace StumblingThroughWPFPartI
{
    public class PersonCollection : ObservableCollection<Person>
    {

        public void LoadAll()
        {
            this.Clear();

            XElement xmlElement = XElement.Load("Data/People.xml");

            foreach (Person person in
                from xmlPerson in xmlElement.Descendants("Person")
                select new Person(xmlPerson.Attribute("FirstName").Value, xmlPerson.Attribute("LastName").Value)
            )
            {
                this.Add(person);
            }
        }
    }
}

 

 

Nothing new here, just using the 'Person' object and xml data.  Let's see how 'AddressCollection.LoadByPerson' will work out:

 

using System.Collections.ObjectModel;
using System.Linq;
using System.Xml.Linq;
using System.Collections.Generic;

namespace StumblingThroughWPFPartI
{
    public class AddressCollection : ObservableCollection<Address>
    {
        public void LoadByPerson(Person parent)
        {
            this.Clear();

            XElement xmlElement = XElement.Load("Data/People.xml");
            IEnumerable<XElement> xmlParentElement = from item in xmlElement.Descendants("Person")
                         where item.Attribute("FirstName").Value.Equals(parent.FirstName)
                         where item.Attribute("LastName").Value.Equals(parent.LastName)
                         select item;
            xmlElement = xmlParentElement.First();

            foreach (Address address in
                from xmlAddress in xmlElement.Descendants("Address")
                select new Address(xmlAddress.Attribute("City").Value, xmlAddress.Attribute("State").Value)
            )
            {
                this.Add(address);
            }
        }
    }
}

 

 

All right, things got a little hairy here.  After clearing the list, we need to first find the parent element in the xml data because we are loading all addresses for a given person.  So I load all people, then set an enumerable list (the default return type for a LINQ query) equal to the person element that has both a first and last name equal to the first and last name of the parent person (obviously, we should use a better key here but we'll take what we got for now).  We then set our element equal to the first item in said enumerable list, which should have one and only one item in it if we found our parent correctly.  Once we get the parent, then the rest of the LINQ stuff looks like what we did for the other load methods.

So far, everything has gone pretty smoothly, but we really haven't tried to do anything complicated that utilizes one of the features of LINQ that I find most intriguing.  Let's remedy that by combining our StateCollection object with the xml query used to load an address collection.  I want to grab the state name based on the state abbreviation, and stick it into the address as a read-only property.  We'll first need to modify our Address class' fields, constructor and properties to contain a 'StateName' value (read only property).  Here is the code for the new Address class:

 

using System;
using System.ComponentModel;

namespace StumblingThroughWPFPartI
{
    public class Address : INotifyPropertyChanged, IDataErrorInfo
    {
        String _city;
        String _state;
        String _stateName;

        public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged(System.ComponentModel.PropertyChangedEventArgs e)
        {
            PropertyChanged(this, e);
        }

        public Address(String city, String state, String stateName)
        {
            _city = city;
            _state = state;
            _stateName = stateName;
        }

        public String City
        {
            get
            {
                return _city;
            }
            set
            {
                _city = value;
            }
        }

        public String State
        {
            get
            {
                return _state;
            }
            set
            {
                _state = value;
            }
        }    
        public String StateName
        {
            get
            {
                return _stateName;
            }
        }

        public string Error
        {
            get
            {
                return null;
            }
        }

        public string this[string name]
        {
            get
            {
                string result = null;

                if (name.Equals("City"))
                {
                    if (this._city.Trim().Length == 0)
                        result = "Please specify a city";
                }
                else if (name.Equals("State"))
                {
                    if (this._state.Trim().Length == 0)
                        result = "Please specify a state";
                }

                return result;
            }
        }

    }
}

 

 

With that out of the way, our code will now no longer compile because the 'AddressCollection.LoadByPerson' LINQ statement that instantiates the Address object now requires the 'StateName' parameter in its constructor.  How do we get it?  Well, here is the code, which I will explain afterwards:

 

 

public void LoadByPerson(Person parent)
{
    this.Clear();

    StateCollection allStates = new StateCollection();
    allStates.LoadAll();

    XElement xmlElement = XElement.Load("Data/People.xml");
    IEnumerable<XElement> xmlParentElement = from item in xmlElement.Descendants("Person")
                 where item.Attribute("FirstName").Value.Equals(parent.FirstName)
                 where item.Attribute("LastName").Value.Equals(parent.LastName)
                 select item;
    xmlElement = xmlParentElement.First();

    foreach (Address address in
        from xmlAddress in xmlElement.Descendants("Address")
        join state in allStates on address.Attribute("State").Value equals state.Abbreviation
        select new Address(xmlAddress.Attribute("City").Value, xmlAddress.Attribute("State").Value, state.Name)
    )
    {
        this.Add(address);
    }
}

 

What we do now after clearing the list, is load all of our states into a collection utilizing the methods we already have in place.  We then get the parent 'Person' element same as before.  Now, when we are looping through each address to instantiate an object for it, we join to our States collection by matching the 'State' attribute in the xml of the address to the 'Abbreviated' property of the state objects.  After doing so, the 'state' LINQ element will be the state with the addresses abbreviation, and we can use its Name to pass along to our address constructor.  To prove whether this is working, lets modify our front-end bindings so that the list box that used to be displaying the address' 'City' is now displaying its 'StateName'.  Run the application and we get... an error?  What the heck?  This worked fine in a different project, but now I am getting:

Argument '3': cannot convert from 'lambda expression' to 'System.Func<System.Xml.Linq.XElement,string>'

Well now, let's try and figure out what is going wrong here.  It is clearly an issue with our join, but what?  Oh, duh, my bad... it should join on xmlAddress.Attribute("State"), not address.Attribute("State")... address is our object instance, it doesn't have an 'Attribute' property.  Could have given me better feedback than that!  Change our foreach to this:

 

 

foreach (Address address in
    from xmlAddress in xmlElement.Descendants("Address")
    join state in allStates
    on xmlAddress.Attribute("State").Value equals state.Abbreviation
    select new Address(xmlAddress.Attribute("City").Value, xmlAddress.Attribute("State").Value, state.Name)
)

 

Run the application and we get this:

 

image

 

Now THAT'S what I'm talking about!  This sort of functionality is nothing new to object-oriented programming, but never before has it been so integrated into the language and provided such short and intellisense-enabled coding.  Including custom collections in a query on XML (that could have just as easily been a database, by the way) is very powerful and can help streamline many collection-oriented architectures with various data sources which probably makes up a good chunk of the architectures out there.  I'm sure LINQ can be useful in any other situation as well, maybe not as a replacement for SQL (though definitely can replace XPATH for THIS developer), but at least as a common query language/routine for inter-object queries and interfaces to business objects.  I hope I impressed some of the basics of LINQ to you while giving you a real-world (albeit simple) example of its use.  If not, well, I hope my stumbling through it at least provided you several moments of entertainment even if I didn't chuck a mouse through my monitor (yet).

Comments

Jerry Brunning said:

Nice!  Keep in mind that LINQ can do even more of the work for you.  For example, your code in LoadByPerson can use a single line by using multple From operators:

          XElement xmlElement = XElement.Load("Data/People.xml");

           foreach (var address in (from item in xmlElement.Descendants("Person")

                                                    where item.Attribute("FirstName").Value.Equals(parent.FirstName)

                                                    where item.Attribute("LastName").Value.Equals(parent.LastName)

                                                    from addr in item.Descendants("Address")

                                                    join state in allStates

                                                    on addr.Attribute("State").Value equals state.Abbreviation

                                                    select new Address(addr.Attribute("City").Value, addr.Attribute("State").Value, state.Name))) {

               this.Add(address);

           }

# February 9, 2008 4:18 PM
Leave a Comment

(required) 

(required) 

(optional)

(required)