Dynamisch eigenschappen en waardes in C# bepalen

Ingediend door Dirk Hornstra op 20-okt-2019 20:53

Het "scrapen" van informatie van een website is dat je bepaalde informatie van een website afhaalt en dat op je eigen website gebruikt. Ook prijsvergelijkers gebruiken dit (want je denkt toch niet dat kieskeurig allemaal mensen op kantoor heeft zitten die elke dag websites controleren?). Tenminste, ik mag hopen dat ze dat doodsaaie klusje niet door mensen laten doen ;)

Zo heb ik jaren geleden de domeinnaam wparty.nl vastgelegd, omdat ik daar alle "evenementen" in Nederland vast wilde leggen. Leuk/handig voor toeristen (stel je bent op vakantie in Leeuwarden en je wilt weten of er wat leuks om de omgeving te doen is), maar ook voor ieder ander (welke voorstellingen zijn er in theaters in de buurt, welke artiest die je graag wilt zien/horen treedt waar op)?

Ik heb allemaal sites verzameld, maar dan moet je met bepaalde instellingen aangeven welke "blokken" de informatie bevatten die je wilt hebben. Mijn neef, Jelmer Laverman, heeft daar toen een hoop van verwerkt. En vervolgens heb ik er niets meer mee gedaan, omdat er teveel andere zaken ook nog gedaan moesten worden.... Zonde natuurlijk. Toen mijn neef een tijdje geleden jarig was heb ik me voorgenomen om die site nu eindelijk eens online te zetten en daar zou ik die week mee aan de slag. Toen ik daadwerkelijk de eerste stappen maakte waren we echter alweer een maand verder...

Ik heb nu een werkende omgeving. Het is geen staaltje "clean-code", want dan duurt het nog langer om de boel online te zetten. En ergens tijdens de congressen/events die ik hebt bezocht heb ik gehoord dat "alles wat je bedenkt wordt ook door andere mensen bedacht en uitgevoerd wordt", dus voor iemand anders met het idee aan de haal gaat waar ik al jaren mee aan de slag zou gaan, dan maar los met een site die in de loop van de komende tijd steeds meer aangevuld wordt en verbeterd wordt.

Je hebt de site, maar op een andere locatie heb ik zelf een API draaien die de gegevens ophaalt en verwerkt. En ik heb een eigen simpele back-end-omgeving waarin kan instellen via welk pad zo'n blok met informatie bepaald kan worden, hoe daar de datum uit gefilterd kan worden, de locatie, de omschrijving (naam artiest e.d.). 

Voor het bovenstaande, als je niet weet wat xpath is, stel dat er in een deel van een pagina dit staat:


<div class="row">
<div class="eventdate">12 oktober 2019</div>
<div class="eventtitle">optreden Moai Wark</div>
</div>
<div class="row">
<div class="eventdate">5 november 2019</div>
<div class="eventtitle">optreden 2Unlimited</div>
</div>

In het bovenstaande kan ik aangeven dat voor het opvragen van evenementblokken je het pad  //div[contains(@class,\"row\")] moet volgen. 

Nu werkt het niet allemaal zo simpel. Want zo heb je op de agenda-pagina van het Ziggo Dome in de regel in een blok de dag staan en in een ander deel van dat blok de maand (in 3 letters, dus jan, feb, ...). Je moet dus dat combineren. Maar het jaartal staat helemaal bovenaan dat blok. Dus ook die moet je in een soort xpath-formule kunnen plaatsen en samenvoegen.

Dan heb je vervolgens dat er allemaal spaties voor, tussen of achter komen. Of er staan regelscheidingstekens tussen. Of je hebt een relatieve link naar een detail-pagina (/evenement/details), maar op mijn site zal hier de hele URL geplaatst moeten worden (https://www.naamvandesite/evenement/details).

Nu zijn dat wel herbruikbare zaken, dus ik heb een class gemaakt waarin ik al die eigenschappen/acties kan aanvinken of tekst kan invullen bij een site. Hier een stuk uit de code:


    public class SiteConfiguration
    {
        [Description("Link waar de data opgevraagd wordt.")]
        public string link { get; set; }

        [Description("Locatie: land.")]
        public string location_country { get; set; }

        [Description("Locatie: provincie.")]
        public string location_province { get; set; }

        [Description("Pad voor de items.")]
        public string itempath { get; set; }

        [Description("Pad voor het titel van een item.")]
        public string[] itempath_title { get; set; }

        [Description("Verwijder regeleindes uit de titel van een item.")]
        public bool post_item_title_removelinebreaks { get; set; }

        [Description("Verwijder een deel van de titel met een regex.")]
        public bool post_item_title_filtercontents { get; set; }

        [Description("Reguliere expressie voor het te verwijderen deel van de titel.")]
        public string post_item_title_removeregex { get; set; }

 

En zo breid ik deze class steeds meer uit. Ik kwam bijvoorbeeld een site tegen waarbij in de code 16 Oct. stond, maar het evenement zelf op 17 oktober was (ergens werd met Javascript er een dag bij opgeteld?). Hier heb ik een property string patch_add_day met description "Hack: voeg aantal dagen toe aan gevonden datum".

In deze class zit ook een "verwerken-functie" die al de bovenstaande eigenschappen en acties doorloopt zodat er een goede RSS feed uit rolt. Dat staat allemaal netjes centraal.

Maar hiernaast heb ik een Razor-template waar ik per site deze zaken in kan stellen. Als deze properties moet ik dus op één of andere manier dynamisch kunnen opvragen, want als ik de boel daar fixed in zet, moet ik het op 2 plaatsen vervangen. En gelukkig kan dat op een hele simpele manier. In deze class SiteConfiguration heb ik onderstaande functie toegevoegd:


 

        public PropertyInfo[] GetAllProperties()
        {
            List<PropertyInfo> result = new List<PropertyInfo>();

            Type t = this.GetType();
            result.AddRange(t.GetProperties());

            return result.ToArray();
        }

Als je een site wilt bewerken zorg je in je Controller dat de SiteConfiguration van die site als model wordt meegegeven:


            SiteConfiguration siteConfiguration = null;
            SiteModel siteModel = new SiteModel();
            var site = siteModel.GetSite(id);
            if (site != null)
            {
                string[] sourceData = (string.IsNullOrEmpty(site.SiteData)) ? new string[] { } : site.SiteData.Split(new char[] { '\n' });
                for (int k=0; k < sourceData.Length; k++)
                {
                    sourceData[k] = sourceData[k].TrimEnd(new char[] { '\r' });
                }
                siteConfiguration = new SiteConfiguration(sourceData);
            }
            else
            {
                siteConfiguration = new SiteConfiguration(new string[] { });
            }
            return View(siteConfiguration);

En hieronder zie je hoe je de properties in je Razor-template plaatst en ook meteen de waarde van deze eigenschap bij deze site bepaalt (onderstaande bevat niet alle HTML om leesbaar te houden waar het om draait):


 

@using System.Reflection
@{
    @model SiteConfiguration
}
@{ 
    string labelValue = "";
}
            <table>
                @foreach (PropertyInfo item in Model.GetAllProperties())
                {
                <tr>
                    @{
                        labelValue = item.Name;
                        foreach (var c in item.CustomAttributes)
                        {
                            if (c.AttributeType.Name == "DescriptionAttribute" && c.ConstructorArguments.Count > 0)
                            {
                                labelValue = c.ConstructorArguments[0].Value.ToString();
                            }
                        }
                    }
                    <td>
                        <label for="url">@labelValue</label>
                    </td>
                </tr>
                    <tr>
                        <td>
                            @switch (item.PropertyType.Name.ToLower())
                            {
                                case "boolean":
                                    if ((bool)item.GetValue(Model))
                                    {
                                        <input type="checkbox" name="@item.Name" id="@item.Name" checked="checked" />
                                    }
                                    else
                                    {
                                        <input type="checkbox" name="@item.Name" id="@item.Name" />
                                    }
                                    <br />
                                    break;
                                case "string":
                                    <input type="text" name="@item.Name" id="@item.Name" value="@item.GetValue(Model)" />
                                    <br />
                                    break;
                                case "string[]":
                                    foreach (var s in (string[])item.GetValue(Model))
                                    {
                                        <input type="text" name="@item.Name" value="@s" />
                                    }
                                    <br />
                                    <input type="text" name="@item.Name" id="@item.Name" value="" />
                                    <br />
                                    break;
                                default:
                                    @item.PropertyType.Name;
                                    break;
                            }
                        </td>
                    </tr>
                }
            </table>

En zo kun je dus zoals de titel van deze post zegt "dynamisch eigenschappen en waardes in C# bepalen". Dankzij de description-property kun je een duidelijke uitleg geven waar een variabele voor nodig is/wat deze doet.

Dus www.wparty.nl zal steeds meer sites tonen.

I party, you party, we party! (maar dan zonder de e). :)