Як можна зберегти налаштування програми у програмі Windows Forms?


582

Я хочу досягти дуже просто: у мене є програма Windows Forms (.NET 3.5), яка використовує шлях для зчитування інформації. Цей шлях може бути змінений користувачем, використовуючи форму, яку я надаю.

Тепер я хочу зберегти значення шляху до файлу для подальшого використання. Це було б одне з багатьох налаштувань, збережених у цьому файлі. Цей файл знаходитиметься безпосередньо в папці додатків.

Я розумію, що доступні три варіанти:

  • Файл ConfigurationSettings (appname.exe.config)
  • Реєстр
  • Спеціальний XML-файл

Я прочитав, що файл конфігурації .NET не передбачений для збереження на ньому значень. Що стосується реєстру, то я хотів би від нього якомога далі відійти.

Чи означає це, що я повинен використовувати спеціальний XML-файл для збереження налаштувань конфігурації?

Якщо так, я хотів би побачити приклад коду цього (C #).

Я бачив інші дискусії з цього приводу, але мені це все одно не зрозуміло.


Це програма .NET WinForms? Якщо так, то яку версію .NET ви розробляєте?
Портман

1
Так, це .NET Framework версії 3.5 програми WinForms.
Заправляється

1
чи потрібно зберігати паролі чи значення секретів ? Можливо, потрібне якесь шифрування
Кіквенет

Відповіді:


593

Якщо ви працюєте з Visual Studio, то отримати стабільні налаштування досить легко. Клацніть правою кнопкою миші на проект у Провіднику рішень та виберіть Властивості. Перейдіть на вкладку Налаштування та натисніть на гіперпосилання, якщо налаштування не існує.

На вкладці Налаштування створіть налаштування програми. Visual Studio створює файли Settings.settingsі Settings.Designer.settingsякі містять клас одноплодной , Settingsуспадкований від ApplicationSettingsBase . Ви можете отримати доступ до цього класу зі свого коду для читання / запису параметрів програми:

Properties.Settings.Default["SomeProperty"] = "Some Value";
Properties.Settings.Default.Save(); // Saves settings in application configuration file

Ця методика застосовна як для консолей, Windows Forms, так і для інших типів проектів.

Зверніть увагу , що ви повинні встановити області видимості властивості ваших налаштувань. Якщо ви вибрали область застосування, то Settings.Default. <Ваша властивість> буде лише для читання.

Довідка: Як: Напишіть налаштування користувача під час виконання за допомогою C # - Microsoft Docs


2
якщо у мене є рішення, чи застосовуватиметься це для всього рішення чи для кожного проекту?
franko_camron

8
@Four: Тут у мене є проект .NET 4.0 WinApp, і моя SomeProperty не читається тільки. Settings.Default.SomeProperty = 'value'; Settings.Default.Save();працює як шарм. Або це тому, що у мене є налаштування користувача?
doekman

4
@Four: Коли я змінив налаштування з «Користувач на область застосування» та зберегти файл, я побачив, що в створеному коді сетер зник. Це трапляється і з профілем клієнта 4.0 ...
doekman

3
@Four: чудове посилання, хоча ваше твердження про те, Settings.Default.Save()що нічого не робить, є невірним. Як @aku стверджує у відповіді, налаштування області застосування доступні лише для читання: збереження для них малоефективне. Використовуйте цей користувальницький PortableSettingsProvider для збереження налаштувань для користувача в app.config, розташованому там, де знаходиться exe, а не в папці AppData користувача. Ні, не взагалі добре, але я використовую його під час розробки, щоб використовувати ті самі налаштування від компіляції до компіляції (без нього, вони надходять нові унікальні користувацькі папки з кожною компіляцією).
понеділок

7
На сьогоднішній день, з .NET 3.5, здається, ви можете просто скористатися Settings.Default.SomeProperty, щоб призначити значення та отримати сильну клавіатуру. Крім того, щоб заощадити інший час (у мене знадобився час, щоб розібратися в цьому), потрібно або ввести Properties.Settings.Default, або додати, використовуючи YourProjectNameSpace.Settings у верхній частині вашого файлу. "Налаштування" лише не визначено / не знайдено.
eselk

94

Якщо ви плануєте зберегти файл у тому ж каталозі, що і ваш виконуваний файл, ось приємне рішення, яке використовує формат JSON :

using System;
using System.IO;
using System.Web.Script.Serialization;

namespace MiscConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            MySettings settings = MySettings.Load();
            Console.WriteLine("Current value of 'myInteger': " + settings.myInteger);
            Console.WriteLine("Incrementing 'myInteger'...");
            settings.myInteger++;
            Console.WriteLine("Saving settings...");
            settings.Save();
            Console.WriteLine("Done.");
            Console.ReadKey();
        }

        class MySettings : AppSettings<MySettings>
        {
            public string myString = "Hello World";
            public int myInteger = 1;
        }
    }

    public class AppSettings<T> where T : new()
    {
        private const string DEFAULT_FILENAME = "settings.json";

        public void Save(string fileName = DEFAULT_FILENAME)
        {
            File.WriteAllText(fileName, (new JavaScriptSerializer()).Serialize(this));
        }

        public static void Save(T pSettings, string fileName = DEFAULT_FILENAME)
        {
            File.WriteAllText(fileName, (new JavaScriptSerializer()).Serialize(pSettings));
        }

        public static T Load(string fileName = DEFAULT_FILENAME)
        {
            T t = new T();
            if(File.Exists(fileName))
                t = (new JavaScriptSerializer()).Deserialize<T>(File.ReadAllText(fileName));
            return t;
        }
    }
}

Так, змініть DEFAULT_FILENAME на абсолютний шлях, якщо ви хочете зберегти його в інший каталог. Я думаю, що найчастіше зберігати файл у тій самій каталозі, що і додаток, або в підкаталозі, якщо ви не зберігаєте їх у реєстрі.
Тревор

О, можливо, кращим варіантом було б зберігати файл налаштувань у папці доданих даних користувача.
Тревор

1
Не потрібно мінятись DEFAULT_FILENAME, просто дзвоніть settings.Save(theFileToSaveTo); Будучи усіма шапками, DEFAULT_FILENAMEмає бути постійною . Якщо ви хочете властивість читання-запису, зробіть його та запропонуйте конструктору його встановити DEFAULT_FILENAME. Тоді майте значення аргументу за замовчуванням be null, протестуйте це та використовуйте властивість як значення за замовчуванням. Це трохи більше вводити текст, але дає вам більш стандартний інтерфейс.
Джессі Чісгольм

10
Вам потрібно буде посилатися, System.Web.Extensions.dllякщо ви ще цього не зробили.
ТЕК

9
Я створив цілу бібліотеку на основі цієї відповіді з багатьма вдосконаленнями і зробив її доступною у nuget: github.com/Nucs/JsonSettings
NucS

67

Реєстр заборонено. Ви не впевнені, чи користувач, який використовує вашу програму, має достатньо прав для запису до реєстру.

Ви можете використовувати app.configфайл для збереження налаштувань рівня програми (однакові для кожного користувача, який використовує вашу програму).

Я зберігав би специфічні для користувача налаштування у файлі XML, який буде збережено в Isolated Storage або в каталозі SpecialFolder.ApplicationData .

Поруч із цим., Починаючи з .NET 2.0, можна зберігати значення назад у app.configфайл.


8
Однак, використовуйте реєстр, якщо ви хочете налаштувати для входу / користувача.
thenonhacker

19
Реєстр не портативний
Kb.

10
@thenonhacker: Або використовувати Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData)
Кенні Манн

4
У реєстр користувачів можна записати (багато програм записують туди інформацію, і дозволи користувача ніколи не є проблемою). Перевага використання реєстру перед використанням налаштувань полягає в тому, що якщо у вас є кілька додатків, які діляться однією папкою (скажімо, програма налаштування та програма програми), вони не матимуть однакових налаштувань.
Кесті

3
Основним недоліком Реєстру є важкий спосіб експорту / копіювання налаштувань на інший ПК. Але я не згоден з "Ви не впевнені, чи користувач, який використовує вашу програму, має достатньо прав для запису до реєстру" - У HKEY_CURRENT_USER ви завжди маєте права писати. Це може бути відмовлено, але файлова система також може бути недоступною для поточного користувача (усі можливі папки TEMP тощо).
i486

20

ApplicationSettingsКлас не підтримує збереження налаштувань в app.config файл. Це дуже задумано; програми, які працюють із належним чином захищеним обліковим записом користувача (думаю, що UAC Vista), не мають доступу для запису до інсталяційної папки програми.

Ви можете боротися з системою з ConfigurationManagerкласом. Але тривіальне рішення полягає в тому, щоб зайти в конструктор налаштувань і змінити область налаштування на Користувача. Якщо це спричиняє труднощі (скажімо, налаштування стосується кожного користувача), ви повинні розмістити свою функцію "Опції" в окремій програмі, щоб ви могли запитати запит на підвищення привілеїв. Або відмовитися, використовуючи налаштування.


Не могли б ви розширити своє останнє речення? Попросіть елевацію написати app.config або написати окрему програму, яка проходитиме через домашні папки всіх користувачів, шукайте user.config і редагуйте їх?
CannibalSmith

2
Окрема програма вимагає маніфесту, щоб задати висоту. Щоб знайти належний синтаксис, Google "asinvoker вимагає адмініструтора". Редагування user.config не є практичним і не потрібним.
Ганс Пасант

18

Аргумент реєстру / configSettings / XML все ще здається дуже активним. Я використовував їх усіх, оскільки технологія прогресувала, але моя улюблена базується на системі Threed у поєднанні з Isolated Storage .

Наведений нижче зразок дозволяє зберігати об’єкти з назви властивостей у файлі в ізольованому сховищі. Як от:

AppSettings.Save(myobject, "Prop1,Prop2", "myFile.jsn");

Властивості можна відновити за допомогою:

AppSettings.Load(myobject, "myFile.jsn");

Це лише зразок, а не судження про кращі практики.

internal static class AppSettings
{
    internal static void Save(object src, string targ, string fileName)
    {
        Dictionary<string, object> items = new Dictionary<string, object>();
        Type type = src.GetType();

        string[] paramList = targ.Split(new char[] { ',' });
        foreach (string paramName in paramList)
            items.Add(paramName, type.GetProperty(paramName.Trim()).GetValue(src, null));

        try
        {
            // GetUserStoreForApplication doesn't work - can't identify.
            // application unless published by ClickOnce or Silverlight
            IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForAssembly();
            using (IsolatedStorageFileStream stream = new IsolatedStorageFileStream(fileName, FileMode.Create, storage))
            using (StreamWriter writer = new StreamWriter(stream))
            {
                writer.Write((new JavaScriptSerializer()).Serialize(items));
            }

        }
        catch (Exception) { }   // If fails - just don't use preferences
    }

    internal static void Load(object tar, string fileName)
    {
        Dictionary<string, object> items = new Dictionary<string, object>();
        Type type = tar.GetType();

        try
        {
            // GetUserStoreForApplication doesn't work - can't identify
            // application unless published by ClickOnce or Silverlight
            IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForAssembly();
            using (IsolatedStorageFileStream stream = new IsolatedStorageFileStream(fileName, FileMode.Open, storage))
            using (StreamReader reader = new StreamReader(stream))
            {
                items = (new JavaScriptSerializer()).Deserialize<Dictionary<string, object>>(reader.ReadToEnd());
            }
        }
        catch (Exception) { return; }   // If fails - just don't use preferences.

        foreach (KeyValuePair<string, object> obj in items)
        {
            try
            {
                tar.GetType().GetProperty(obj.Key).SetValue(tar, obj.Value, null);
            }
            catch (Exception) { }
        }
    }
}

1
Або ще краще; використовувати DataContractJsonSerializer
Boczek

16

Я хотів поділитися створеною для цього бібліотекою. Це крихітна бібліотека, але велике поліпшення (IMHO) над файлами .settings.

Бібліотека називається Jot (GitHub) . Ось стара стаття проекту «Код проекту», про яку я писав про неї.

Ось як ви використовуєте його для відстеження розміру та розташування вікна:

public MainWindow()
{
    InitializeComponent();

    _stateTracker.Configure(this)
        .IdentifyAs("MyMainWindow")
        .AddProperties(nameof(Height), nameof(Width), nameof(Left), nameof(Top), nameof(WindowState))
        .RegisterPersistTrigger(nameof(Closed))
        .Apply();
}

Перевага порівняно з файлами .settings: Там значно менше коду, і це набагато менше схильних до помилок, оскільки вам потрібно згадати кожне властивість лише один раз .

З файлами налаштувань вам потрібно згадати кожну властивість п’ять разів: один раз, коли ви явно створюєте властивість, і ще чотири рази в коді, який копіює значення вперед і назад.

Зберігання, серіалізація тощо можна повністю налаштувати. Коли цільові об'єкти створені контейнером IoC , ви можете [підключити його] [], щоб він автоматично застосував відстеження до всіх об'єктів, які він вирішує, так що все, що вам потрібно зробити, щоб властивість було стійким, - це ударити по [Trackable] атрибут на ньому.

Це легко налаштовується, і ви можете налаштувати: - коли дані зберігаються та застосовуються в усьому світі або для кожного відстежуваного об'єкта - як вони серіалізуються - де вони зберігаються (наприклад, файл, база даних, онлайн, ізольоване сховище, реєстр) - правила, які можуть скасувати застосування / збереження даних для ресурсу

Повірте, бібліотека - це найвищий рівень!


14

Простий спосіб - використовувати об’єкт даних конфігурації, зберегти його як XML-файл із іменем програми в локальній папці та під час запуску прочитати його назад.

Ось приклад для зберігання позиції та розміру форми.

Конфігураційний об'єкт даних сильно набраний та простий у використанні:

[Serializable()]
public class CConfigDO
{
    private System.Drawing.Point m_oStartPos;
    private System.Drawing.Size m_oStartSize;

    public System.Drawing.Point StartPos
    {
        get { return m_oStartPos; }
        set { m_oStartPos = value; }
    }

    public System.Drawing.Size StartSize
    {
        get { return m_oStartSize; }
        set { m_oStartSize = value; }
    }
}

Клас менеджера для збереження та завантаження:

public class CConfigMng
{
    private string m_sConfigFileName = System.IO.Path.GetFileNameWithoutExtension(System.Windows.Forms.Application.ExecutablePath) + ".xml";
    private CConfigDO m_oConfig = new CConfigDO();

    public CConfigDO Config
    {
        get { return m_oConfig; }
        set { m_oConfig = value; }
    }

    // Load configuration file
    public void LoadConfig()
    {
        if (System.IO.File.Exists(m_sConfigFileName))
        {
            System.IO.StreamReader srReader = System.IO.File.OpenText(m_sConfigFileName);
            Type tType = m_oConfig.GetType();
            System.Xml.Serialization.XmlSerializer xsSerializer = new System.Xml.Serialization.XmlSerializer(tType);
            object oData = xsSerializer.Deserialize(srReader);
            m_oConfig = (CConfigDO)oData;
            srReader.Close();
        }
    }

    // Save configuration file
    public void SaveConfig()
    {
        System.IO.StreamWriter swWriter = System.IO.File.CreateText(m_sConfigFileName);
        Type tType = m_oConfig.GetType();
        if (tType.IsSerializable)
        {
            System.Xml.Serialization.XmlSerializer xsSerializer = new System.Xml.Serialization.XmlSerializer(tType);
            xsSerializer.Serialize(swWriter, m_oConfig);
            swWriter.Close();
        }
    }
}

Тепер ви можете створити екземпляр і використовувати для завантаження та закриття подій вашої форми:

    private CConfigMng oConfigMng = new CConfigMng();

    private void Form1_Load(object sender, EventArgs e)
    {
        // Load configuration
        oConfigMng.LoadConfig();
        if (oConfigMng.Config.StartPos.X != 0 || oConfigMng.Config.StartPos.Y != 0)
        {
            Location = oConfigMng.Config.StartPos;
            Size = oConfigMng.Config.StartSize;
        }
    }

    private void Form1_FormClosed(object sender, FormClosedEventArgs e)
    {
        // Save configuration
        oConfigMng.Config.StartPos = Location;
        oConfigMng.Config.StartSize = Size;
        oConfigMng.SaveConfig();
    }

І створений XML-файл також читається:

<?xml version="1.0" encoding="utf-8"?>
<CConfigDO xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <StartPos>
    <X>70</X>
    <Y>278</Y>
  </StartPos>
  <StartSize>
    <Width>253</Width>
    <Height>229</Height>
  </StartSize>
</CConfigDO>

1
У мене це чудово працює в розробці, але коли я розгортаю програму, середній користувач не має доступу до c:\program files\my applicationпапки, тому збереження налаштувань призводить до помилки. Я замість цього намагаюся зберегти XML-файл у AppData, але мені просто цікаво, чи існує очевидний спосіб подолання цієї проблеми, оскільки такий підхід, здається, спрацював для вас.
Філіп Стратфорд

@PhilipStratford Оскільки це лише звичайний файл, його можна зберегти в будь-якому місці. Просто знайдіть місце з доступом до запису.
Дітер Мемкен

@PhilipStratford Можливо, папка AppData - це варіант для вас, див. C # отримання шляху% AppData% , як згадує кайт .
Dieter Meemken

Дякую, я вже реалізував це, зберігаючи файл xml у папці AppDate. Я просто задумався, чи існує простий спосіб зберегти його у папці програми згідно з вашим прикладом, оскільки я припускав, що ви змусили його працювати. Не хвилюйтесь, папка AppData, мабуть, краще місце в будь-якому випадку!
Філіп Стратфорд


5

Інші параметри, замість використання користувальницького XML-файлу, ми можемо використовувати більш зручний формат файлу: JSON або YAML-файл.

  • Якщо ви використовуєте .NET 4.0 динамічний, ця бібліотека дійсно проста у використанні (серіалізувати, десеріалізувати, підтримувати вкладені об'єкти та замовляти вихід, як хочете + об'єднання декількох налаштувань до одного) JsonConfig (використання еквівалентне ApplicationSettingsBase)
  • Для бібліотеки конфігурації .NET YAML ... Я не знайшов такої простої у використанні, як JsonConfig

Ви можете зберігати файл налаштувань у декількох спеціальних папках (для всіх користувачів та кожного користувача), як перераховано тут Середовище. Перерахування спеціальних папок та декілька файлів (лише для читання, за роллю, за користувача та ін.)

Якщо ви вирішили скористатися кількома налаштуваннями, ви можете об'єднати ці налаштування: Наприклад, об'єднання параметрів за замовчуванням + BasicUser + AdminUser. Ви можете використовувати свої власні правила: останнє перевизначає значення тощо.


4

"Чи означає це, що я повинен використовувати спеціальний XML-файл для збереження налаштувань конфігурації?" Ні, не обов’язково. Ми використовуємо SharpConfig для таких операцій.

Наприклад, якщо файл конфігурації такий

[General]
# a comment
SomeString = Hello World!
SomeInteger = 10 # an inline comment

Ми можемо отримати такі значення

var config = Configuration.LoadFromFile("sample.cfg");
var section = config["General"];

string someString = section["SomeString"].StringValue;
int someInteger = section["SomeInteger"].IntValue;

Він сумісний з .NET 2.0 і вище. Ми можемо створювати конфігураційні файли на ходу, і можемо зберегти їх згодом.

Джерело: http://sharpconfig.net/
GitHub: https://github.com/cemdervis/SharpConfig


3

Наскільки я можу сказати, .NET підтримує постійні налаштування за допомогою вбудованого засобу налаштування додатків:

Функція "Налаштування додатків" для Windows Forms дозволяє легко створювати, зберігати та підтримувати налаштування користувальницьких додатків та користувачів на клієнтському комп'ютері. За допомогою налаштувань програми Windows Forms ви можете зберігати не тільки дані програми, такі як рядки підключення до бази даних, але й дані, що стосуються користувачів, наприклад, налаштування користувацьких програм. Використовуючи Visual Studio або користувальницький керований код, ви можете створювати нові налаштування, читати їх і записувати їх на диск, прив'язувати їх до властивостей ваших форм і перевіряти дані налаштувань перед завантаженням і збереженням. - http://msdn.microsoft.com/en-us/library/k4s6c3a0.aspx


2
Неправда .. див. Відповідь Аку вище. його можливо, використовуючи Settings and ApplicationSettingsBase
Gishu

2

Іноді потрібно позбутися тих налаштувань, які зберігаються у традиційному файлі web.config або app.config. Ви хочете більш точного контролю над розгортанням своїх налаштувань і розділеним дизайном даних. Або вимога полягає в тому, щоб дозволити додавати нові записи під час виконання.

Я можу уявити два хороших варіанти:

  • Сильно набрана версія та
  • Об'єктно-орієнтована версія.

Перевагою сильно набраної версії є сильно набрані імена параметрів і значення. Не існує ризику змішування імен або типів даних. Недоліком є ​​те, що більше параметрів потрібно кодувати, їх не можна додавати під час виконання.

Перевага для об'єктно-орієнтованої версії полягає в тому, що нові параметри можна додавати під час виконання. Але ви не маєте сильно набраних імен та значень. Потрібно бути обережними з ідентифікаторами рядків. Повинен знати тип даних, збережений раніше при отриманні значення.

Ви можете знайти код обох повністю функціональних реалізацій ТУТ .


2

Так, конфігурацію можна зберегти - але це в значній мірі залежить від способу, який ви вирішите це зробити. Дозвольте мені описати технічні відмінності, щоб ви могли зрозуміти наявні варіанти:

По- перше, потрібно розрізняти, чи хочете ви використовувати applicationSettings або AppSettings в вашому *.exe.config(він же App.configв Visual Studio) файл - є принципові відмінності, які описані тут .

Обидва пропонують різні способи збереження змін:

  • У AppSettings дозволяють читати і записувати безпосередньо в конфігураційний файл ( з допомогою config.Save(ConfigurationSaveMode.Modified);, де конфігурації визначається як config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);).

  • Налаштування програми дозволяють читати, але якщо ви пишете зміни (через Properties.Settings.Default.Save();), вони записуються на основі кожного користувача, зберігаються в спеціальному місці (наприклад C:\Documents and Settings\USERID\Local Settings\Application Data\FIRMNAME\WindowsFormsTestApplicati_Url_tdq2oylz33rzq00sxhvxucu5edw2oghw\1.0.0.0). Як згадував Ганс Пасант у своїй відповіді, це тому, що користувач, як правило, обмежує права на програмні файли і не може писати на нього, не викликаючи підказку UAC. Недоліком є ​​те, що якщо ви надалі додаєте конфігураційні ключі, вам потрібно синхронізувати їх із кожним профілем користувача.

Примітка. Як зазначено у запитанні, є 3-й варіант: Якщо ви ставитесь до файлу конфігурації як XML-документа, ви можете завантажити, змінити та зберегти його за допомогою System.Xml.Linq.XDocumentкласу. Використовувати нестандартний XML-файл не потрібно, ви можете прочитати існуючий конфігураційний файл; для елементів запиту ви навіть можете використовувати запити Linq. Я наводив приклад тут , перевірте функцію GetApplicationSettingтам у відповіді.

Якщо вам потрібно шифрувати для захисту своїх значень, ознайомтеся з цією відповіддю.


Дякую за чудову відповідь.
NoChance

2
@NoChance - Ласкаво просимо, радий, що я можу тобі допомогти!
Метт

Приємно! Нарешті все зрозумів 😂😂😂
Моморо

1
@Momoro - приємно чути! ;-)
Метт

1
public static class SettingsExtensions
{
    public static bool TryGetValue<T>(this Settings settings, string key, out T value)
    {
        if (settings.Properties[key] != null)
        {
            value = (T) settings[key];
            return true;
        }

        value = default(T);
        return false;
    }

    public static bool ContainsKey(this Settings settings, string key)
    {
        return settings.Properties[key] != null;
    }

    public static void SetValue<T>(this Settings settings, string key, T value)
    {
        if (settings.Properties[key] == null)
        {
            var p = new SettingsProperty(key)
            {
                PropertyType = typeof(T),
                Provider = settings.Providers["LocalFileSettingsProvider"],
                SerializeAs = SettingsSerializeAs.Xml
            };
            p.Attributes.Add(typeof(UserScopedSettingAttribute), new UserScopedSettingAttribute());
            var v = new SettingsPropertyValue(p);
            settings.Properties.Add(p);
            settings.Reload();
        }
        settings[key] = value;
        settings.Save();
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.