Vlastní konfigurační sekce v .NET

Tomáš Holan       10. 12. 2013       Komponenty, .NET       6316 zobrazení

Nedávno byl na dotNETportalu dotaz týkající se konfigurace a kromě Settings File byli zmíněné i vlastní konfigurační sekce (Configuration Sections). Přestože jsem je za ta léta použil již hodně krát, uvědomil jsem si, že jsem o nich ještě nepsal. Nyní se to pokusím napravit.

Místo toho, abych ale detailně popisoval, který atribut nastaví ve výsledné XML konfiguraci to nebo ono, zde raději uvedu pár příkladů konfiguračních sekcí z praxe (detaily lze dohledat například v dokumentaci na MSDN).

Vlastní konfigurační sekce

Nejprve ale ještě chvilku obecně. Vlastní konfigurační sekce lze v .NETu psát již od verze 2.0. Každá konfigurační sekce je definována třídou poděděnou ze základní třídy ConfigurationSection z namespace System.Configuration. Pokud konfigurace kromě obyčejných vlastností obsahuje i další vnořené elementy, ty jsou definované dalšími třídami, které jsou poděděné z ConfigurationElement nebo ConfigurationElementCollection, pokud se jedná o kolekci. Konfigurační elementy mohou opět obsahovat obyčejné vlastností nebo další vnořené elementy.

Konfigurační sekci pak odpovídá část XML konfigurace v konfiguračním souboru app.config nebo web.config aplikace. Struktura tohoto XML konfigurace je námi zvolená tj. odpovídá tomu jak je definovaná v implementaci konfigurační sekce. Aby aplikace danou konfigurační sekci mohla použít, musí být přímo v .config souboru sekce zaregistrována. V registraci se uvádí název třídy včetně namespace a plný název assembly obsahující třídu konfigurační sekce. Jedná o tedy buď o assembly nějaké knihovny nebo případně assembly aplikace samotné.

ExchangeRatesConfigurationSection

První příklad ukazuje konfiguraci popisující nastavení, pro které zdrojové/domácí měny a v jaký čas má být prováděn automatický import kurzovních lístků z České národní banky (pro CZK), Národní banky slovenska (EUR) nebo Magyar Nemzeti Bank (HUF).

Příklad konfigurace včetně registrace konfigurační sekce vypadá takto:

<configuration>
  <configSections>
    <section name="exchangeRates" type="IMP.CNBService.ExchangeRatesConfigurationSection, CNBService" />
  </configSections>

  <exchangeRates>
    <currency code="CZK" time="15:00:00"/>
    <currency code="EUR" time="15:30:00"/>
    <currency code="HUF" time="13:30:00"/>
  </exchangeRates>
</configuration>

Implementace konfigurační sekce ExchangeRatesConfigurationSection je následující:

using System;
using System.Configuration;
using System.Collections.Generic;

namespace IMP.CNBService
{
    internal class ExchangeRatesConfigurationSection : ConfigurationSection
    {
        #region action methods
        [ConfigurationProperty("", IsDefaultCollection = true)]
        public CurrencyCollection Currency
        {
            get { return (CurrencyCollection)base[""]; }
        }
        #endregion
    }

    internal abstract class ConfigurationElementCollection<TKey, TElement> : ConfigurationElementCollection, IEnumerable<TElement> where TElement : ConfigurationElement, new()
    {
        #region constructors and destructors
        public ConfigurationElementCollection() { }
        #endregion

        #region action methods
        protected abstract TKey GetElementKey(TElement element);

        public void Add(TElement element)
        {
            this.BaseAdd(element);
        }

        public void Remove(TKey key)
        {
            this.BaseRemove(key);
        }

        public new IEnumerator<TElement> GetEnumerator()
        {
            foreach (TElement element in (ConfigurationElementCollection)this)
            {
                yield return element;
            }
        }
        #endregion

        #region property getters/setters
        public abstract override ConfigurationElementCollectionType CollectionType { get; }
        protected abstract override string ElementName { get; }

        public TElement this[TKey key]
        {
            get
            {
                return (TElement)this.BaseGet(key);
            }
        }

        public TElement this[int index]
        {
            get
            {
                return (TElement)this.BaseGet(index);
            }
        }
        #endregion

        #region private member functions
        protected override ConfigurationElement CreateNewElement()
        {
            return new TElement();
        }

        protected override object GetElementKey(ConfigurationElement element)
        {
            return this.GetElementKey((TElement)element);
        }
        #endregion
    }

    internal class CurrencyCollection : ConfigurationElementCollection<string, CurrencyConfigurationElement>
    {
        #region property getters/setters
        public override ConfigurationElementCollectionType CollectionType
        {
            get { return ConfigurationElementCollectionType.BasicMap; }
        }

        protected override string ElementName
        {
            get { return "currency"; }
        }
        #endregion

        #region private member functions
        protected override string GetElementKey(CurrencyConfigurationElement element)
        {
            return element.Code;
        }
        #endregion
    }

    internal class CurrencyConfigurationElement : ConfigurationElement
    {
        #region property getters/setters
        /// <summary>
        /// Mezinárodní trojpísmenný kód zdrojové/domácí měny (měna pro kterou se kurzy načítají, CZK, EUR nebo HUF)
        /// </summary>
        [ConfigurationProperty("code", IsKey = true, IsRequired = true)]
        public string Code
        {
            get
            {
                return (string)this["code"];
            }
            private set
            {
                this["code"] = value;
            }
        }

        /// <summary>
        /// Doba spuštění načtení kurzů
        /// </summary>
        [ConfigurationProperty("time", IsRequired = true)]
        public TimeSpan Time
        {
            get
            {
                return (TimeSpan)this["time"];
            }
            private set
            {
                this["time"] = value;
            }
        }
        #endregion
    }
}

A použití této konfigurace interně ve službě provádějící vlastní načítání kurzů vypadá nějak takto:

var configuration = ((ExchangeRatesConfigurationSection)System.Configuration.ConfigurationManager.GetSection("exchangeRates")).Currency;

foreach (IExchangeRateProvider provider in GetAllProviders())
{
    var element = configuration[provider.CurrencyCode];
    if (element != null)
    {
        ConfigureProvider(provider, element.Time);
    }
}

SignCertificateConfigurationSection

Tato konfigurační sekce popisuje konfiguraci certifikátu používaného například pro podepisování generovaných PDF souborů. Tato konfigurační sekce byla již použita v článku zde.

Příklad této konfigurace je:

<configuration>
  <configSections>
    <section name="signCertificateConfiguration" type="IMP.Cryptography.SignCertificateConfigurationSection" />
  </configSections>
   
  <signCertificateConfiguration>
    <signCertificate x509FindType="FindByThumbprint" findValue="862002e527e1ea0e3465d59cc4c49af0d093ad0c" storeLocation="LocalMachine" storeName="My" />
  </signCertificateConfiguration>
</configuration>

A implementace konfigurační sekce SignCertificateConfigurationSection i pomocné třídy SignCertificateConfiguration, která konfiguraci zpřístupňuje, je:

using System;
using System.Security.Cryptography.X509Certificates;
 
namespace IMP.Cryptography
{
    internal class SignCertificateConfigurationSection : System.Configuration.ConfigurationSection
    {
        #region constants
        private const string cSignCertificateElementName = "signCertificate";
        #endregion

        #region property getters/setters
        public X509Certificate2 SignCertificate
        {
            get
            {
                var element = this.SignCertificateReference;
                if (string.IsNullOrWhiteSpace(element.FindValue))
                {
                    throw new System.Configuration.ConfigurationErrorsException("Sign certificate configuration is missing.");
                }
 
                return CertificateUtil.GetValidCertificate(element.StoreName, element.StoreLocation, element.X509FindType, element.FindValue);
            }
        }
        #endregion
 
        #region private member functions
        [ConfigurationProperty(cSignCertificateElementName, IsRequired = true)]
        private System.ServiceModel.Configuration.CertificateReferenceElement SignCertificateReference
        {
            get { return (System.ServiceModel.Configuration.CertificateReferenceElement)this[cSignCertificateElementName]; }
        }
        #endregion
    }
 
    internal static class SignCertificateConfiguration
    {
        #region property getters/setters
        public static X509Certificate2 SignCertificate
        {
            get
            {
                var configuration = (SignCertificateConfigurationSection)System.Configuration.ConfigurationManager.GetSection("signCertificateConfiguration");
                if (configuration == null)
                {
                    throw new System.Configuration.ConfigurationErrorsException("Configuration section 'signCertificateConfiguration' not found.");
                }
 
                return configuration.SignCertificate;
            }
        }
        #endregion
    }
}

Popis použití pomocné třídy CertificateUtil i třídu samotnou naleznete u původního článku.

PasswordPoliciesConfigurationSection

Posledním příkladem je konfigurační sekce PasswordPoliciesConfigurationSection, která může sloužit pro nastavení pravidel pro kontrolu sily hesla. Konfigurace obsahuje dvě pravidla, jedno používané pro běžné uživatele (defaultPasswordPolicy) a druhé používané například ve správě uživatelů pro nastavení hesla administrátorem aplikace (setPasswordPolicy).

Příklad konfigurace vypadá takto:

<configuration>
    <configSections>
       <section name="passwordPolicies" type="IMP.Security.PasswordPoliciesConfigurationSection, IMP.Security, Version=1.0.0.0, Culture=neutral, PublicKeyToken=21f74bb59b6740fd"/>
    </configSections>
    
    <passwordPolicies>
        <defaultPasswordPolicy minRequiredPasswordLength="5" minRequiredUppercaseCharacters="1" validateUserName="false"/>
        <setPasswordPolicy minRequiredPasswordLength="0" minRequiredUppercaseCharacters="0">
           <invalidPasswords>
              <remove value="abc123"/>
              <remove value="heslo"/>
              <add value="1234567890"/>
           </invalidPasswords>
        </setPasswordPolicy>
    </passwordPolicies>
</configuration>

Implementace této konfigurační sekce je:

using System;
using System.Configuration;
using System.Collections.Generic;

namespace IMP.Security
{
    internal class PasswordPoliciesConfigurationSection : ConfigurationSection
    {
        #region property getters/setters
        [ConfigurationProperty("defaultPasswordPolicy")]
        public PasswordPolicyElement DefaultPasswordPolicy
        {
            get
            {
                return (PasswordPolicyElement)this["defaultPasswordPolicy"];
            }
        }

        [ConfigurationProperty("setPasswordPolicy")]
        public PasswordPolicyElement SetPasswordPolicy
        {
            get
            {
                return (PasswordPolicyElement)this["setPasswordPolicy"];
            }
        }
        #endregion
    }

    internal class PasswordPolicyElement : ConfigurationElement
    {
        #region constants
        internal static readonly string[] DefaultInvalidPasswords = new[] { "123", "1234", "12345", "123456", "1234567", "12345678", "123456789", "abc123", "heslo" };
        #endregion

        #region constructors and destructors
        public PasswordPolicyElement()
        {
            foreach (string s in DefaultInvalidPasswords)
            {
                this.InvalidPasswords.Add(s);
            }
        }
        #endregion

        #region property getters/setters
        /// <summary>
        /// Minimální délka hesla (výchozí hodnota je 6)
        /// </summary>
        [ConfigurationProperty("minRequiredPasswordLength")]
        public int? MinRequiredPasswordLength
        {
            get
            {
                return (int?)this["minRequiredPasswordLength"];
            }
        }

        /// <summary>
        /// Minimální počet velkých písmen v heslu (výchozí hodnota je 0)
        /// </summary>
        [ConfigurationProperty("minRequiredUppercaseCharacters")]
        public int? MinRequiredUppercaseCharacters
        {
            get
            {
                return (int?)this["minRequiredUppercaseCharacters"];
            }
        }

        /// <summary>
        /// Minimální počet malých písmen v heslu (výchozí hodnota je 0)
        /// </summary>
        [ConfigurationProperty("minRequiredLowercaseCharacters")]
        public int? MinRequiredLowercaseCharacters
        {
            get
            {
                return (int?)this["minRequiredLowercaseCharacters"];
            }
        }

        /// <summary>
        /// Minimální počet číslic v heslu (výchozí hodnota je 0)
        /// </summary>
        [ConfigurationProperty("minRequiredDigits")]
        public int? MinRequiredDigits
        {
            get
            {
                return (int?)this["minRequiredDigits"];
            }
        }

        /// <summary>
        /// Minimální počet nonalfanumerických znaků v heslu (výchozí hodnota je 0)
        /// </summary>
        [ConfigurationProperty("minRequiredNonAlphanumericCharacters")]
        public int? MinRequiredNonAlphanumericCharacters
        {
            get
            {
                return (int?)this["minRequiredNonAlphanumericCharacters"];
            }
        }

        /// <summary>
        /// PasswordStrengthRegularExpression
        /// </summary>
        [ConfigurationProperty("passwordStrengthRegularExpression", DefaultValue = null)]
        public string PasswordStrengthRegularExpression
        {
            get
            {
                return (string)this["passwordStrengthRegularExpression"];
            }
        }

        /// <summary>
        /// Kontrola zda heslo není shodné s přihlašovacím jménem nebo neobsahuje jméno či příjmení (výchozí hodnota je true)
        /// </summary>
        [ConfigurationProperty("validateUserName")]
        public bool? ValidateUserName
        {
            get
            {
                return (bool?)this["validateUserName"];
            }
        }

        /// <summary>
        /// Seznam zakázaných hesel (výchozí hodnota je "123;1234;12345;123456;1234567;12345678;123456789;abc123;heslo")
        /// </summary>
        [ConfigurationProperty("invalidPasswords")]
        [ConfigurationCollection(typeof(ValueConfigurationCollection<string>), AddItemName = "add", ClearItemsName = "clear", RemoveItemName = "remove")]
        public ValueConfigurationCollection<string> InvalidPasswords
        {
            get
            {
                return (ValueConfigurationCollection<string>)base["invalidPasswords"];
            }
        }

        internal bool HasDefaultInvalidPasswords
        {
            get { return Enumerable.SequenceEqual(this.InvalidPasswords, DefaultInvalidPasswords); }
        }
        #endregion
    }

    internal class ValueConfigurationElement<T> : ConfigurationElement
    {
        #region property getters/setters
        [ConfigurationProperty("value", IsKey = true, IsRequired = true)]
        public T Value
        {
            get { return (T)this["value"]; }
            set { this["value"] = value; }
        }
        #endregion
    }

    internal class ValueConfigurationCollection<T> : ConfigurationElementCollection, ICollection<T>
    {
        #region action methods
        public void Clear()
        {
            BaseClear();
        }

        public bool Contains(T value)
        {
            return BaseGet(value) != null;
        }

        public void Add(T value)
        {
            BaseAdd(new ValueConfigurationElement<T>() { Value = value }, true);
        }

        public bool Remove(T value)
        {
            BaseRemove(value);
            return true;
        }

        public void CopyTo(T[] array, int index)
        {
            if (array == null)
            {
                throw new ArgumentNullException("array");
            }

            foreach (ValueConfigurationElement<T> element in (ConfigurationElementCollection)this)
            {
                array.SetValue(element.Value, index++);
            }
        }

        public new IEnumerator<T> GetEnumerator()
        {
            foreach (ValueConfigurationElement<T> element in (ConfigurationElementCollection)this)
            {
                yield return element.Value;
            }
        }
        #endregion

        #region property getters/setters
        bool ICollection<T>.IsReadOnly
        {
            get { return IsReadOnly(); }
        }
        #endregion

        #region private member functions
        protected override ConfigurationElement CreateNewElement()
        {
            return new ValueConfigurationElement<T>();
        }

        protected override object GetElementKey(ConfigurationElement element)
        {
            return ((ValueConfigurationElement<T>)element).Value;
        }
        #endregion
    }
}

Použití této konfigurace je trochu složitější, protože zde budeme chtít, aby element setPasswordPolicy mohl obsahovat pouze rozdíly oproti defaultPasswordPolicy:

internal sealed class PasswordPolicy
{
    #region member varible and default property initialization
    public int MinRequiredPasswordLength { get; private set; }
    public int MinRequiredUppercaseCharacters { get; private set; }
    public int MinRequiredLowercaseCharacters { get; private set; }
    public int MinRequiredDigits { get; private set; }
    public int MinRequiredNonAlphanumericCharacters { get; private set; }
    public string PasswordStrengthRegularExpression { get; private set; }
    public bool ValidateUserName { get; private set; }
    public ISet<string> InvalidPasswords { get; private set; }
    #endregion

    #region constructors and destructors
    internal PasswordPolicy(PasswordPolicyElement passwordPolicyElement)
    {
        this.MinRequiredPasswordLength = passwordPolicyElement.MinRequiredPasswordLength ?? 6;
        this.MinRequiredUppercaseCharacters = passwordPolicyElement.MinRequiredUppercaseCharacters ?? 0;
        this.MinRequiredLowercaseCharacters = passwordPolicyElement.MinRequiredLowercaseCharacters ?? 0;
        this.MinRequiredDigits = passwordPolicyElement.MinRequiredDigits ?? 0;
        this.MinRequiredNonAlphanumericCharacters = passwordPolicyElement.MinRequiredNonAlphanumericCharacters ?? 0;
        this.PasswordStrengthRegularExpression = passwordPolicyElement.PasswordStrengthRegularExpression ?? "";
        this.ValidateUserName = passwordPolicyElement.ValidateUserName ?? true;

        this.InvalidPasswords = new HashSet<string>(passwordPolicyElement.InvalidPasswords);
    }

    internal PasswordPolicy(PasswordPolicy defautPolicy, PasswordPolicyElement passwordPolicyElement)
    {
        this.MinRequiredPasswordLength = passwordPolicyElement.MinRequiredPasswordLength ?? defautPolicy.MinRequiredPasswordLength;
        this.MinRequiredUppercaseCharacters = passwordPolicyElement.MinRequiredUppercaseCharacters ?? defautPolicy.MinRequiredUppercaseCharacters;
        this.MinRequiredLowercaseCharacters = passwordPolicyElement.MinRequiredLowercaseCharacters ?? defautPolicy.MinRequiredLowercaseCharacters;
        this.MinRequiredDigits = passwordPolicyElement.MinRequiredDigits ?? defautPolicy.MinRequiredDigits;
        this.MinRequiredNonAlphanumericCharacters = passwordPolicyElement.MinRequiredNonAlphanumericCharacters ?? defautPolicy.MinRequiredNonAlphanumericCharacters;
        this.PasswordStrengthRegularExpression = !string.IsNullOrEmpty(passwordPolicyElement.PasswordStrengthRegularExpression) ? passwordPolicyElement.PasswordStrengthRegularExpression : defautPolicy.PasswordStrengthRegularExpression;
        this.ValidateUserName = passwordPolicyElement.ValidateUserName ?? defautPolicy.ValidateUserName;

        this.InvalidPasswords = new HashSet<string>(!passwordPolicyElement.HasDefaultInvalidPasswords ? (IEnumerable<string>)passwordPolicyElement.InvalidPasswords : defautPolicy.InvalidPasswords);
    }
    #endregion
}

var configuration = (PasswordPoliciesConfigurationSection)System.Configuration.ConfigurationManager.GetSection("passwordPolicies");
var defaultPasswordPolicy = new PasswordPolicy(configuration.DefaultPasswordPolicy);
var setPasswordPolicy = new PasswordPolicy(defaultPasswordPolicy, configuration.SetPasswordPolicy);

Další vlastní konfigurační sekce byla také zde na dotNETportalu implementována v tomto článku.

Implementací vlastních konfiguračních sekcí se dále zabývají například tyto zdroje:

http://msdn.microsoft.com/en-us/library/2tw134k3.aspx
http://www.codeproject.com/Articles/16466/Unraveling-the-Mysteries-of-NET-2-0-Configuration
http://robseder.wordpress.com/articles/the-complete-guide-to-custom-configuration-sections
http://www.codeproject.com/Articles/32490/Custom-Configuration-Sections-for-Lazy-Coders
http://www.codeproject.com/Articles/20548/Creating-a-Custom-Configuration-Section-in-C

 

hodnocení článku

0       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

Code

mozem Vas poprosit aj o link na stiahnutie celeho projektu, dakujem

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Použití

Chybí mi tu jedna zásadní věc a to příklad použití. Z definice tříd nemusí být každému jasné, jak se to potom používá.

Také se mi nikdy nepodařilo udělat vlastní ConfigurationElement tak, aby ho šlo korektně používat v návrháři konfiguračních hodnot.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

U prvního a posledního příkladu kód na použití dané konfigurace mám (alespoň na její přístup), u druhého příkladu byl již v odkazovaném článku.

Co je "návrhář konfiguračních hodnot"?

nahlásit spamnahlásit spam 0 odpovědětodpovědět

To je ohromně efektivní klikací nástroj na vytváření konfiguračních hodnot, dostupný ve vlastnostech projektu na kartě Settings.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Na záložce Settings lze editovat pouze Application Settings, které (natvrdo) využívají konfigurační element <applicationSettings> nebo <userSettings> .config souboru.

Není to obecný editor .config souborů a tudíž jiné konfigurační sekce editovat neumí.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Vlastní ConfigurationElementCollection lze vnořit do jedné z těchto sekcí a mým cílem bylo zpřístupnit ho v tomto návrháři.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Tak s tím zkušenosti nemám, vždy jsem psal celou konfigurační sekci.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Vysvětlení

Je tam hromada kódu, který je vysvětlován jako celek. Článek by šel rozdělit na 3 a popsat jako debilovi, což by začátečníkovi hodně pomohlo.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Ano, kód sice opravdu není podrobně vysvětlen, ale myslím si, že by to nemělo být až tolik na škodu. Ty příklady nejsou příliš složité a z ukázkou konfigurace, kterou popisují, se v nich dá zorientovat.

Dohromady se v nich vyskytuje (nebo opakuje) jak udělat nejběžnější prvky konfigurace:

-Jednoduchou vlastnost - v XML konfiguraci atribut.

-Vnořený konfigurační element odvozený ze základní třídy ConfigurationElement - v XML vnořený element např. PasswordPoliciesConfigurationSection.DefaultPasswordPolicy typu PasswordPolicyElement.

-Kolekci typu ConfigurationElementCollectionType.BasicMap (http://msdn.microsoft.com/en-us/library/... ) - v XML několik stejně pojmenovaných elementů viz ExchangeRatesConfigurationSection.Currency.

-Kolekci typu ConfigurationElementCollectionType.AddRemoveClearMap (ten je ve třídě odvozené ze základní třídy ConfigurationElementCollection nastavený jako výchozí) - v XML elementy Add/Remove/Clear viz PasswordPolicyElement.InvalidPasswords.

-Výchozí kolekci s ConfigurationPropertyAttribute.IsDefaultCollection = true (http://msdn.microsoft.com/en-us/library/... ) tj. kolekci, která nemá své prvky obalené v samostatném XML elementu viz. rozdíl mezi ExchangeRatesConfigurationSection.Currency (IsDefaultCollection = true prvky jsou rovnou uvnitř konfigurační sekce tj. v elementu <exchangeRates>) a PasswordPolicyElement.InvalidPasswords (prvky obalené v elementu <invalidPasswords> díky nastavení ConfigurationPropertyAttribute.Name = "invalidPasswords").

nahlásit spamnahlásit spam 0 odpovědětodpovědět

No já si hlavně myslím, že učit se přesně kterou třídu podědit z jaký a pak ty třídy propojit apod. nemá v tomhle případě cenu. Člověk to nedělá zas každý den. Když potřebuji udělat konfigurační sekci, tak je nejjednodušší způsob vzít nejpodobnější jinou už hotovou, a tu podle toho předělat na to co je potřeba (hlavně přejmenovat elementy a vlastnosti). Problém ale nastane, pokud žádnou takovou nemáme, nebo musíme složitě hledat. A právě k tomu tento článek "pouze" s uvedeným kódem úplně vyhovuje. Dá se z toho hodně dobře vyjít a pouze upravit uvedený kód.

nahlásit spamnahlásit spam 0 odpovědětodpovědět
                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:  
Heslo:  

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback