This project is read-only.

ItemData

Jun 6, 2013 at 7:10 AM
A few changes here.

The Items property that gets ALL items from the server was scary. Accessing that property would cause over 25,000 API calls in order to get the details of all items. I don't think it ever makes sense to do this, or at least not in a way that simply accessing a property can cause it to happen on the caller's behalf, so I removed that property and the IEnumerable implementation. If we really need to provide the details of all items to the caller, it would make more sense to retrieve it once, serialize it to XML, and save it to disk. Then just read that file each time we run. We could give the caller the option to update the XML file with a call that explicitly does that. Let me know if you want me to implement that. But a property accessor that causes 25,000 API calls every time a program runs is really, really bad. :)

This class also had a similar caching problem as the GuildData class. If anything was in the cache, it assumed that that was everything that existed, so if an item wasn't in the cache, it didn't return it. Now it checks the cache first and then hits the API.
Jun 6, 2013 at 10:44 AM
I went ahead and updated ItemData again to add a serialize-to-disk caching mechanism. It implements IEnumerable again, and AllItems is back. I had to use a binary serializer because the IEnumerables won't serialize to Xml. Here's how it works:

When the constructor is called, it immediately looks for a cache file on disk. If present, it checks the build number recorded there and compares it to the build of the live server. If the build numbers match, it loads all the item details from that file, and we're done. We don't have to make any API calls at all for items.

If the file is not there or if the build number doesn't match, then the first time the caller tries to do anything such as enumerate items or retrieve one by ID, we query for all item IDs and all item details and cache them. This can take a minute or two and amounts to about 17 MB of data. We write that data to disk along with the build number.

From that point on, we'll have this data cached on disk and we'll continue to use that cache file every time the program runs until the build number no longer matches.

For any application making heavy use of item details, this should be a much better approach. Let me know what you think.

We should probably do the same thing for recipes.
Jun 6, 2013 at 10:48 AM
Oh and all these changes are in the new Dev branch, not the main branch.
Jun 6, 2013 at 12:30 PM
I'm not a fan of enforcing a caching mechanism onto the user. I want to give them a much freedom as possible with the exploitation of the data from the server. This said: I'm against that the user has to use the binary file caching.

However I'm aware that 2500 api calls is way too much and takes way too much time. This said my plan was to create one default "cache to file" service and offer the user an interface where he can create his own "cache to file" service.

I'm going to work something out and include that in the main branch. Until that is implemented however please use your current implementation further.
Jun 6, 2013 at 4:25 PM
ok, I did some thinking and this was the first I could come up with
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="IDataFileAdapter.cs" company="GW2.Net Coding Team">
//   This product is licensed under the GNU General Public License version 2 (GPLv2) as defined on the following page: http://www.gnu.org/licenses/gpl-2.0.html
// </copyright>
// <summary>
//   Defines the IDataToFile type.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace GW2DotNET.V1.Infrastructure.Adapters
{
    /// <summary>
    /// The DataFileAdapter interface.
    /// </summary>
    /// <typeparam name="T">The type of the item to load from / save into the cache.</typeparam>
    public interface IDataFileAdapter<T> 
    {
        /// <summary>
        /// Checks if the stored data is still compatible with the latest build.
        /// </summary>
        /// <param name="currentVersion">
        ///  The current Version.
        /// </param>
        /// <returns>
        /// true if the data is still valid for the current build of the game, otherwise false.
        /// </returns>
        bool IsStillUpToDate(int currentVersion);

        /// <summary>
        /// Writes the cached data to the disk.
        /// </summary>
        /// <param name="dataToWrite">The data to write.</param>
        void WriteToFile(T dataToWrite);

        /// <summary>
        /// The read from file.
        /// </summary>
        /// <returns>
        /// The <see cref="T"/> loaded from the disk.
        /// </returns>
        T ReadFromFile();
    }
}
If a user wants to build it's own "save to disk" he has to implement this interface. we then offer in the data providers to save the data to the disk via this interface. This would give us the freedom just to return the interface without having to worry how the user wants to save it to the disk, be it json, xml plain text or some other obscure format.

What do yo think?
Jun 6, 2013 at 8:51 PM
Ok, I changed the thing on my home pc a bit. I now don't use an interface but define an abstract class from which the user has to inherit. This has the advantage that I can force the user to implement a load and save method but on the other hand give him a build comparison method at the same time.
This is the code, anything else I should include there?
namespace GW2DotNET.V1.Infrastructure
{
    /// <summary>
    /// The data to file base.
    /// </summary>
    /// <typeparam name="T">
    /// The type to save or load. 
    /// </typeparam>
    public abstract class DataToFileBase<T>
    {
        /// <summary>
        /// Checks if the build is still the latest build.
        /// </summary>
        /// <param name="currentBuild">
        /// The build that the data was created with.
        /// </param>
        /// <returns>
        /// True if the supplied build number is still the latest build, otherwise false.
        /// </returns>
        public virtual bool IsStillUpToDate(int currentBuild)
        {
            return currentBuild == ApiCall.Build;
        }

        /// <summary>
        /// Saves the supplied date to the file system.
        /// </summary>
        /// <param name="dataToSave">
        /// The data to save.
        /// </param>
        public abstract void SaveToFile(T dataToSave);

        /// <summary>
        /// Loads the data from the file system into the cache. 
        /// </summary>
        /// <returns>
        /// The <see cref="T"/> loaded from the file system..
        /// </returns>
        public abstract T LoadFromFile();
    }
}
Btw: I wrote a Data-To-Xml serialize so we can serialize to xml or binary.
Jun 7, 2013 at 5:29 AM
I'm not sure I like the idea of leaving this much implementation up to the caller. What if we just expose a method on ItemData like CacheToFile(string path, FileFormat format) and LoadCacheFile(string path).

How'd you get it to serialize to Xml? XmlSerializer won't work with interfaces.
Jun 7, 2013 at 11:37 AM
That's why we offer them one default write to file method.

As for serializing: Use LINQToXml. I had to write most of the serializing part by hand, but I'm gradually automating it.