Định dạng dữ liệu thuộc tính enum cho ComboBox trong WPF


256

Ví dụ lấy mã sau:

public enum ExampleEnum { FooBar, BarFoo }

public class ExampleClass : INotifyPropertyChanged
{
    private ExampleEnum example;

    public ExampleEnum ExampleProperty 
    { get { return example; } { /* set and notify */; } }
}

Tôi muốn một cơ sở dữ liệu tài sản exampleProperty cho ComboBox, để nó hiển thị các tùy chọn "FooBar" và "BarFoo" và hoạt động ở chế độ TwoWay. Tối ưu tôi muốn định nghĩa ComboBox của tôi trông giống như thế này:

<ComboBox ItemsSource="What goes here?" SelectedItem="{Binding Path=ExampleProperty}" />

Hiện tại tôi có các trình xử lý cho các sự kiện ComboBox.SelectionChanged và exampleClass.PropertyChanged được cài đặt trong Cửa sổ của tôi nơi tôi thực hiện liên kết thủ công.

Có một cách tốt hơn hoặc một số cách kinh điển? Bạn có thường sử dụng Bộ chuyển đổi không và làm thế nào để bạn tạo ra ComboBox với các giá trị phù hợp? Tôi thậm chí không muốn bắt đầu với i18n ngay bây giờ.

Biên tập

Vì vậy, một câu hỏi đã được trả lời: Làm cách nào để điền vào ComboBox với các giá trị phù hợp.

Lấy các giá trị Enum dưới dạng danh sách các chuỗi thông qua ObjectDataProvider từ phương thức Enum.GetValues ​​tĩnh:

<Window.Resources>
    <ObjectDataProvider MethodName="GetValues"
        ObjectType="{x:Type sys:Enum}"
        x:Key="ExampleEnumValues">
        <ObjectDataProvider.MethodParameters>
            <x:Type TypeName="ExampleEnum" />
        </ObjectDataProvider.MethodParameters>
    </ObjectDataProvider>
</Window.Resources>

Cái này tôi có thể sử dụng làm ItemSource cho ComboBox của mình:

<ComboBox ItemsSource="{Binding Source={StaticResource ExampleEnumValues}}"/>

4
Tôi đã khám phá điều này và có một giải pháp mà bạn có thể sử dụng (hoàn thành với bản địa hóa) trong WPF nằm ở đây .
ageektrapping

Câu trả lời:


208

Bạn có thể tạo một phần mở rộng đánh dấu tùy chỉnh.

Ví dụ về cách sử dụng:

enum Status
{
    [Description("Available.")]
    Available,
    [Description("Not here right now.")]
    Away,
    [Description("I don't have time right now.")]
    Busy
}

Ở đầu XAML của bạn:

    xmlns:my="clr-namespace:namespace_to_enumeration_extension_class

và sau đó...

<ComboBox 
    ItemsSource="{Binding Source={my:Enumeration {x:Type my:Status}}}" 
    DisplayMemberPath="Description" 
    SelectedValue="{Binding CurrentStatus}"  
    SelectedValuePath="Value"  /> 

Và việc thực hiện ...

public class EnumerationExtension : MarkupExtension
  {
    private Type _enumType;


    public EnumerationExtension(Type enumType)
    {
      if (enumType == null)
        throw new ArgumentNullException("enumType");

      EnumType = enumType;
    }

    public Type EnumType
    {
      get { return _enumType; }
      private set
      {
        if (_enumType == value)
          return;

        var enumType = Nullable.GetUnderlyingType(value) ?? value;

        if (enumType.IsEnum == false)
          throw new ArgumentException("Type must be an Enum.");

        _enumType = value;
      }
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
      var enumValues = Enum.GetValues(EnumType);

      return (
        from object enumValue in enumValues
        select new EnumerationMember{
          Value = enumValue,
          Description = GetDescription(enumValue)
        }).ToArray();
    }

    private string GetDescription(object enumValue)
    {
      var descriptionAttribute = EnumType
        .GetField(enumValue.ToString())
        .GetCustomAttributes(typeof (DescriptionAttribute), false)
        .FirstOrDefault() as DescriptionAttribute;


      return descriptionAttribute != null
        ? descriptionAttribute.Description
        : enumValue.ToString();
    }

    public class EnumerationMember
    {
      public string Description { get; set; }
      public object Value { get; set; }
    }
  }

7
@Gregor S. what my: Enumulation là gì?
joshua

14
@Crown 'my' là tiền tố không gian tên mà bạn khai báo ở đầu tệp xaml của bạn: vd: xmlns: my = "clr-namepace: namepace_to_enumutions_extension_ class. Enumutions là viết tắt của EnumutionsExtension, trong xaml bạn không phải viết toàn bộ tên mở rộng .
Gregor Slavec

33
+1, nhưng số lượng mã mà WPF yêu cầu để thực hiện những điều đơn giản nhất thực sự là tiêu đề
Konrad Morawski

1
Tôi thực sự không thích cách nó khiến bạn sử dụng một tham chiếu đến một phần của mô hình của bạn - kiểu liệt kê - trong chế độ xem, trong thông số ItemsSource. Để giữ cho chế độ xem và mô hình được tách rời, tôi cần tạo một bản sao của bảng liệt kê trong ViewModel và mã ViewModel để dịch giữa hai ... Điều này sẽ làm cho giải pháp không còn đơn giản nữa. Hoặc có cách nào để tự cung cấp loại từ ViewModel không?
lampak

6
Một hạn chế khác là bạn không thể làm điều này nếu bạn có nhiều ngôn ngữ.
River-Claire Williamson

176

Trong chế độ xem, bạn có thể có:

public MyEnumType SelectedMyEnumType 
{
    get { return _selectedMyEnumType; }
    set { 
            _selectedMyEnumType = value;
            OnPropertyChanged("SelectedMyEnumType");
        }
}

public IEnumerable<MyEnumType> MyEnumTypeValues
{
    get
    {
        return Enum.GetValues(typeof(MyEnumType))
            .Cast<MyEnumType>();
    }
}

Trong XAML các ItemSourceliên kết đến MyEnumTypeValuesSelectedItemliên kết với SelectedMyEnumType.

<ComboBox SelectedItem="{Binding SelectedMyEnumType}" ItemsSource="{Binding MyEnumTypeValues}"></ComboBox>

Điều này đã làm việc tuyệt vời trong ứng dụng Universal của tôi và rất dễ thực hiện. Cảm ơn bạn!
Nathan Strutz

96

Tôi không thích sử dụng tên của enum trong UI. Tôi thích sử dụng giá trị khác nhau cho người dùng ( DisplayMemberPath) và khác nhau cho giá trị (enum trong trường hợp này) ( SelectedValuePath). Hai giá trị này có thể được đóng gói đểKeyValuePair và lưu trữ trong từ điển.

XAML

<ComboBox Name="fooBarComboBox" 
          ItemsSource="{Binding Path=ExampleEnumsWithCaptions}" 
          DisplayMemberPath="Value" 
          SelectedValuePath="Key"
          SelectedValue="{Binding Path=ExampleProperty, Mode=TwoWay}" > 

C #

public Dictionary<ExampleEnum, string> ExampleEnumsWithCaptions { get; } =
    new Dictionary<ExampleEnum, string>()
    {
        {ExampleEnum.FooBar, "Foo Bar"},
        {ExampleEnum.BarFoo, "Reversed Foo Bar"},
        //{ExampleEnum.None, "Hidden in UI"},
    };


private ExampleEnum example;
public ExampleEnum ExampleProperty
{
    get { return example; }
    set { /* set and notify */; }
}

EDIT: Tương thích với mẫu MVVM.


14
Tôi nghĩ rằng câu trả lời của bạn bị đánh giá thấp, có vẻ như tùy chọn tốt nhất được đưa ra những gì ComboBox mong đợi. Có lẽ bạn có thể đặt một trình xây dựng từ điển trong getter Enum.GetValues, nhưng điều đó sẽ không giải quyết được phần nào của tên sẽ được hiển thị. Cuối cùng, và đặc biệt nếu I18n được triển khai, bạn sẽ phải thay đổi công cụ theo cách thủ công nếu enum thay đổi. Nhưng enums không nên thay đổi thường xuyên, nếu có, phải không? +1
heltonbiker

2
Câu trả lời này là tuyệt vời VÀ nó cho phép bản địa hóa các mô tả enums ... Cảm ơn vì điều này!
Shay

2
Giải pháp này rất tốt vì nó xử lý cả enum và bản địa hóa với ít mã hơn các giải pháp khác!
hfann

2
Vấn đề với Dictionary là các khóa được sắp xếp theo giá trị băm nên có rất ít quyền kiểm soát đối với điều đó. Mặc dù dài dòng hơn một chút, tôi đã sử dụng Danh sách <KeyValuePair <enum, chuỗi >> để thay thế. Ý kiến ​​hay.
Kevin Brock

3
@CoperNick @Pragmateek sửa lỗi mới:public Dictionary<ExampleEnum, string> ExampleEnumsWithCaptions { get; } = new Dictionary<ExampleEnum, string>() { {ExampleEnum.FooBar, "Foo Bar"}, {ExampleEnum.BarFoo, "Reversed Foo Bar"}, //{ExampleEnum.None, "Hidden in UI"}, };
Jinjinov

40

Tôi không biết nếu chỉ có thể sử dụng XAML nhưng hãy thử các cách sau:

Đặt tên cho ComboBox của bạn để bạn có thể truy cập vào tên mã: "typeComboBox1"

Bây giờ hãy thử như sau

typesComboBox1.ItemsSource = Enum.GetValues(typeof(ExampleEnum));

24

Dựa trên câu trả lời được chấp nhận nhưng hiện đã bị xóa do ageektrapping cung cấp, tôi đã tạo ra một phiên bản rút gọn mà không có một số tính năng nâng cao hơn. Tất cả các mã được bao gồm ở đây để cho phép bạn sao chép-dán nó và không bị chặn bởi rot-link.

Tôi sử dụng cái System.ComponentModel.DescriptionAttributemà thực sự được dành cho mô tả thời gian thiết kế. Nếu bạn không thích sử dụng thuộc tính này, bạn có thể tạo thuộc tính của riêng mình nhưng tôi nghĩ rằng việc sử dụng thuộc tính này thực sự hoàn thành công việc. Nếu bạn không sử dụng thuộc tính, tên sẽ mặc định là tên của giá trị enum trong mã.

public enum ExampleEnum {

  [Description("Foo Bar")]
  FooBar,

  [Description("Bar Foo")]
  BarFoo

}

Đây là lớp được sử dụng làm nguồn vật phẩm:

public class EnumItemsSource : Collection<String>, IValueConverter {

  Type type;

  IDictionary<Object, Object> valueToNameMap;

  IDictionary<Object, Object> nameToValueMap;

  public Type Type {
    get { return this.type; }
    set {
      if (!value.IsEnum)
        throw new ArgumentException("Type is not an enum.", "value");
      this.type = value;
      Initialize();
    }
  }

  public Object Convert(Object value, Type targetType, Object parameter, CultureInfo culture) {
    return this.valueToNameMap[value];
  }

  public Object ConvertBack(Object value, Type targetType, Object parameter, CultureInfo culture) {
    return this.nameToValueMap[value];
  }

  void Initialize() {
    this.valueToNameMap = this.type
      .GetFields(BindingFlags.Static | BindingFlags.Public)
      .ToDictionary(fi => fi.GetValue(null), GetDescription);
    this.nameToValueMap = this.valueToNameMap
      .ToDictionary(kvp => kvp.Value, kvp => kvp.Key);
    Clear();
    foreach (String name in this.nameToValueMap.Keys)
      Add(name);
  }

  static Object GetDescription(FieldInfo fieldInfo) {
    var descriptionAttribute =
      (DescriptionAttribute) Attribute.GetCustomAttribute(fieldInfo, typeof(DescriptionAttribute));
    return descriptionAttribute != null ? descriptionAttribute.Description : fieldInfo.Name;
  }

}

Bạn có thể sử dụng nó trong XAML như thế này:

<Windows.Resources>
  <local:EnumItemsSource
    x:Key="ExampleEnumItemsSource"
    Type="{x:Type local:ExampleEnum}"/>
</Windows.Resources>
<ComboBox
  ItemsSource="{StaticResource ExampleEnumItemsSource}"
  SelectedValue="{Binding ExampleProperty, Converter={StaticResource ExampleEnumItemsSource}}"/> 

23

Sử dụng ObjectDataProvider:

<ObjectDataProvider x:Key="enumValues"
   MethodName="GetValues" ObjectType="{x:Type System:Enum}">
      <ObjectDataProvider.MethodParameters>
           <x:Type TypeName="local:ExampleEnum"/>
      </ObjectDataProvider.MethodParameters>
 </ObjectDataProvider>

và sau đó liên kết với tài nguyên tĩnh:

ItemsSource="{Binding Source={StaticResource enumValues}}"

Tìm giải pháp này tại blog này


Câu trả lời tốt đẹp. Ngẫu nhiên, nó giúp bạn không phải lo lắng về Convertervấn đề enum-to-string.
DonBoitnott

1
Giải pháp liên kết dường như đã chết (Văn bản tiếng Hàn hoặc tiếng Nhật?). Nếu tôi đặt mã của bạn vào Tài nguyên XAML thì có nghĩa là Enum không được hỗ trợ trong dự án WPF.
Sebastian

6

Cách ưa thích của tôi để thực hiện điều này là ValueConverterđể cả ItemSource và chọnValue liên kết với cùng một thuộc tính. Điều này không yêu cầu các thuộc tính bổ sung để giữ cho ViewModel của bạn đẹp và sạch sẽ.

<ComboBox ItemsSource="{Binding Path=ExampleProperty, Converter={x:EnumToCollectionConverter}, Mode=OneTime}"
          SelectedValuePath="Value"
          DisplayMemberPath="Description"
          SelectedValue="{Binding Path=ExampleProperty}" />

Và định nghĩa của Bộ chuyển đổi:

public static class EnumHelper
{
  public static string Description(this Enum e)
  {
    return (e.GetType()
             .GetField(e.ToString())
             .GetCustomAttributes(typeof(DescriptionAttribute), false)
             .FirstOrDefault() as DescriptionAttribute)?.Description ?? e.ToString();
  }
}

[ValueConversion(typeof(Enum), typeof(IEnumerable<ValueDescription>))]
public class EnumToCollectionConverter : MarkupExtension, IValueConverter
{
  public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
  {
    return Enum.GetValues(value.GetType())
               .Cast<Enum>()
               .Select(e => new ValueDescription() { Value = e, Description = e.Description()})
               .ToList();
  }
  public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
  {
    return null;
  }
  public override object ProvideValue(IServiceProvider serviceProvider)
  {
    return this;
  }
}

Bộ chuyển đổi này sẽ làm việc với bất kỳ enum. ValueDescriptionchỉ là một lớp học đơn giản với một Valuetài sản và một Descriptiontài sản. Bạn có thể cũng giống như dễ dàng sử dụng một Tuplevới Item1Item2, hoặc KeyValuePairvới KeyValuethay vì giá trị gia tăng và mô tả hoặc bất kỳ lớp khác của sự lựa chọn của bạn miễn là nó có thể chứa một giá trị enum và chuỗi mô tả về giá trị enum.


Câu trả lời tốt đẹp! Đối với ValueDescriptionlớp, Descriptiontài sản có thể được bỏ qua nếu không cần thiết. Một lớp học đơn giản chỉ có Valuetài sản cũng hoạt động!
pogosama

Ngoài ra, nếu bạn muốn liên kết với RadioButton, thì phương thức Convert phải trả về một danh sách các chuỗi, tức là .Select(e => e.ToString())thay vì sử dụng ValueDescriptionlớp.
pogosama

Thay vì ValueDescriptioncũng KeyValuePaircó thể được sử dụng, như được hiển thị ở đây
Apfelkuacha

5

Đây là một giải pháp chung sử dụng phương pháp trợ giúp. Điều này cũng có thể xử lý một enum của bất kỳ loại cơ bản nào (byte, sbyte, uint, long, v.v.)

Phương pháp trợ giúp:

static IEnumerable<object> GetEnum<T>() {
    var type    = typeof(T);
    var names   = Enum.GetNames(type);
    var values  = Enum.GetValues(type);
    var pairs   =
        Enumerable.Range(0, names.Length)
        .Select(i => new {
                Name    = names.GetValue(i)
            ,   Value   = values.GetValue(i) })
        .OrderBy(pair => pair.Name);
    return pairs;
}//method

Xem mô hình:

public IEnumerable<object> EnumSearchTypes {
    get {
        return GetEnum<SearchTypes>();
    }
}//property

ComboBox:

<ComboBox
    SelectedValue       ="{Binding SearchType}"
    ItemsSource         ="{Binding EnumSearchTypes}"
    DisplayMemberPath   ="Name"
    SelectedValuePath   ="Value"
/>

5

bạn có thể xem xét một cái gì đó như thế:

  1. xác định một kiểu cho textblock hoặc bất kỳ điều khiển nào bạn muốn sử dụng để hiển thị enum của bạn:

    <Style x:Key="enumStyle" TargetType="{x:Type TextBlock}">
        <Setter Property="Text" Value="&lt;NULL&gt;"/>
        <Style.Triggers>
            <Trigger Property="Tag">
                <Trigger.Value>
                    <proj:YourEnum>Value1<proj:YourEnum>
                </Trigger.Value>
                <Setter Property="Text" Value="{DynamicResource yourFriendlyValue1}"/>
            </Trigger>
            <!-- add more triggers here to reflect your enum -->
        </Style.Triggers>
    </Style>
  2. xác định phong cách của bạn cho ComboBoxItem

    <Style TargetType="{x:Type ComboBoxItem}">
        <Setter Property="ContentTemplate">
            <Setter.Value>
                <DataTemplate>
                    <TextBlock Tag="{Binding}" Style="{StaticResource enumStyle}"/>
                </DataTemplate>
            </Setter.Value>
        </Setter>
    </Style>
  3. thêm một hộp tổ hợp và tải nó với các giá trị enum của bạn:

    <ComboBox SelectedValue="{Binding Path=your property goes here}" SelectedValuePath="Content">
        <ComboBox.Items>
            <ComboBoxItem>
                <proj:YourEnum>Value1</proj:YourEnum>
            </ComboBoxItem>
        </ComboBox.Items>
    </ComboBox>

nếu enum của bạn lớn, tất nhiên bạn có thể làm tương tự trong mã, không cần phải gõ nhiều. Tôi thích cách tiếp cận đó, vì nó làm cho việc bản địa hóa trở nên dễ dàng - bạn xác định tất cả các mẫu một lần và sau đó, bạn chỉ cập nhật các tệp tài nguyên chuỗi của mình.


ChọnValuePath = "Nội dung" đã giúp tôi ở đây. Tôi có ComboBoxItems dưới dạng giá trị chuỗi và tiếp tục không thể chuyển đổi ComboBoxItem thành Loại Enum của mình. Cảm ơn
adriaanp

2

Nếu bạn đang sử dụng MVVM, dựa trên câu trả lời @rudigrobler, bạn có thể làm như sau:

Thêm thuộc tính sau vào lớp ViewModel

public Array ExampleEnumValues => Enum.GetValues(typeof(ExampleEnum));

Sau đó, trong XAML làm như sau:

<ComboBox ItemsSource="{Binding ExampleEnumValues}" ... />

1

Đây là một DevExpresscâu trả lời cụ thể dựa trên câu trả lời được bình chọn hàng đầu bởiGregor S. (hiện tại nó có 128 phiếu).

Điều này có nghĩa là chúng ta có thể giữ kiểu dáng nhất quán trên toàn bộ ứng dụng:

nhập mô tả hình ảnh ở đây

Thật không may, câu trả lời ban đầu không hoạt động với ComboBoxEdit DevExpress mà không có một số sửa đổi.

Đầu tiên, XAML cho ComboBoxEdit:

<dxe:ComboBoxEdit ItemsSource="{Binding Source={xamlExtensions:XamlExtensionEnumDropdown {x:myEnum:EnumFilter}}}"
    SelectedItem="{Binding BrokerOrderBookingFilterSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
    DisplayMember="Description"
    MinWidth="144" Margin="5" 
    HorizontalAlignment="Left"
    IsTextEditable="False"
    ValidateOnTextInput="False"
    AutoComplete="False"
    IncrementalFiltering="True"
    FilterCondition="Like"
    ImmediatePopup="True"/>

Không cần phải nói, bạn sẽ cần trỏ xamlExtensionsvào không gian tên chứa lớp mở rộng XAML (được định nghĩa bên dưới):

xmlns:xamlExtensions="clr-namespace:XamlExtensions"

Và chúng ta phải chỉ myEnumvào không gian tên chứa enum:

xmlns:myEnum="clr-namespace:MyNamespace"

Sau đó, enum:

namespace MyNamespace
{
    public enum EnumFilter
    {
        [Description("Free as a bird")]
        Free = 0,

        [Description("I'm Somewhat Busy")]
        SomewhatBusy = 1,

        [Description("I'm Really Busy")]
        ReallyBusy = 2
    }
}

Vấn đề với XAML là chúng tôi không thể sử dụng SelectedItemValue, vì điều này gây ra lỗi vì trình cài đặt không thể truy cập được (phần giám sát của bạn, DevExpress). Vì vậy, chúng tôi phải sửa đổi ViewModelđể có được giá trị trực tiếp từ đối tượng:

private EnumFilter _filterSelected = EnumFilter.All;
public object FilterSelected
{
    get
    {
        return (EnumFilter)_filterSelected;
    }
    set
    {
        var x = (XamlExtensionEnumDropdown.EnumerationMember)value;
        if (x != null)
        {
            _filterSelected = (EnumFilter)x.Value;
        }
        OnPropertyChanged("FilterSelected");
    }
}

Để đầy đủ, đây là phần mở rộng XAML từ câu trả lời ban đầu (được đổi tên một chút):

namespace XamlExtensions
{
    /// <summary>
    ///     Intent: XAML markup extension to add support for enums into any dropdown box, see http://bit.ly/1g70oJy. We can name the items in the
    ///     dropdown box by using the [Description] attribute on the enum values.
    /// </summary>
    public class XamlExtensionEnumDropdown : MarkupExtension
    {
        private Type _enumType;


        public XamlExtensionEnumDropdown(Type enumType)
        {
            if (enumType == null)
            {
                throw new ArgumentNullException("enumType");
            }

            EnumType = enumType;
        }

        public Type EnumType
        {
            get { return _enumType; }
            private set
            {
                if (_enumType == value)
                {
                    return;
                }

                var enumType = Nullable.GetUnderlyingType(value) ?? value;

                if (enumType.IsEnum == false)
                {
                    throw new ArgumentException("Type must be an Enum.");
                }

                _enumType = value;
            }
        }

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            var enumValues = Enum.GetValues(EnumType);

            return (
                from object enumValue in enumValues
                select new EnumerationMember
                       {
                           Value = enumValue,
                           Description = GetDescription(enumValue)
                       }).ToArray();
        }

        private string GetDescription(object enumValue)
        {
            var descriptionAttribute = EnumType
                .GetField(enumValue.ToString())
                .GetCustomAttributes(typeof (DescriptionAttribute), false)
                .FirstOrDefault() as DescriptionAttribute;


            return descriptionAttribute != null
                ? descriptionAttribute.Description
                : enumValue.ToString();
        }

        #region Nested type: EnumerationMember
        public class EnumerationMember
        {
            public string Description { get; set; }
            public object Value { get; set; }
        }
        #endregion
    }
}

Tuyên bố miễn trừ trách nhiệm: Tôi không có liên kết với DevExpress. Telerik cũng là một thư viện tuyệt vời.


Đối với hồ sơ, tôi không liên kết với DevExpress. Telerik cũng có các thư viện rất tốt và kỹ thuật này thậm chí có thể không cần thiết cho thư viện của họ.
Contango

0

Hãy thử sử dụng

<ComboBox ItemsSource="{Binding Source={StaticResource ExampleEnumValues}}"
    SelectedValue="{Binding Path=ExampleProperty}" />

Điều này không hoạt động. Hộp tổ hợp sẽ chỉ hiển thị một văn bản trống và thay đổi nó sẽ không làm gì cả. Tôi đoán ném vào một công cụ chuyển đổi ở đây sẽ là giải pháp tốt nhất.
Maximilian

0

Tôi đã tạo một dự án CodePlex mã nguồn mở thực hiện việc này. Bạn có thể tải gói NuGet từ đây .

<enumComboBox:EnumComboBox EnumType="{x:Type demoApplication:Status}" SelectedValue="{Binding Status}" />
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.