Comment lier un enum à un contrôle combobox dans WPF?

j'essaie de trouver un exemple simple où les enums sont présentés comme est. Tous les exemples que j'ai vus essayent d'ajouter de jolies chaînes d'affichage, mais je ne veux pas de cette complexité.

fondamentalement, j'ai une classe qui détient toutes les propriétés que je lie, en mettant d'abord le DataContext à cette classe, puis en spécifiant la liaison comme ceci dans le fichier xaml:

<ComboBox ItemsSource="{Binding Path=EffectStyle}"/>

mais cela ne montre pas les valeurs enum dans le ComboBox comme article.

148
demandé sur Andy 2011-05-27 02:32:54

15 réponses

vous pouvez le faire à partir du code en plaçant le code suivant dans la fenêtre Loaded event handler, par exemple:

yourComboBox.ItemsSource = Enum.GetValues(typeof(EffectStyle)).Cast<EffectStyle>();

si vous devez le lier dans XAML, vous devez utiliser ObjectDataProvider pour créer un objet disponible comme source de liaison:

<Window x:Class="YourNamespace.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:System="clr-namespace:System;assembly=mscorlib"
        xmlns:StyleAlias="clr-namespace:Motion.VideoEffects">
    <Window.Resources>
        <ObjectDataProvider x:Key="dataFromEnum" MethodName="GetValues"
                            ObjectType="{x:Type System:Enum}">
            <ObjectDataProvider.MethodParameters>
                <x:Type TypeName="StyleAlias:EffectStyle"/>
            </ObjectDataProvider.MethodParameters>
        </ObjectDataProvider>
    </Window.Resources>
    <Grid>
        <ComboBox ItemsSource="{Binding Source={StaticResource dataFromEnum}}"
                  SelectedItem="{Binding Path=CurrentEffectStyle}" />
    </Grid>
</Window>

attirer l'attention sur le code suivant:

xmlns:System="clr-namespace:System;assembly=mscorlib"
xmlns:StyleAlias="clr-namespace:Motion.VideoEffects"

Guide comment cartographier l'espace de noms et l'assemblage vous pouvez lire sur MSDN .

245
répondu Kyrylo M 2015-05-21 18:13:35

j'aime que tous les objets que je lie soient définis dans mon ViewModel , donc j'essaie d'éviter d'utiliser <ObjectDataProvider> dans le xaml quand c'est possible.

ma solution n'utilise aucune donnée définie dans la vue et aucun code-arrière. Seulement un DataBinding, un ValueConverter réutilisable, une méthode pour obtenir une collection de descriptions pour n'importe quel type D'Enum, et une propriété unique dans le ViewModel à lier.

Quand je veux lier un Enum à un ComboBox le texte que je veux afficher ne correspond jamais aux valeurs du Enum , donc j'utilise l'attribut [Description()] pour lui donner le texte que je veux réellement voir dans le ComboBox . Si j'avais un enum de classes de personnages dans un jeu, cela ressemblerait à quelque chose comme ceci:

public enum PlayerClass
{
  // add an optional blank value for default/no selection
  [Description("")]
  NOT_SET = 0,
  [Description("Shadow Knight")]
  SHADOW_KNIGHT,
  ...
}

J'ai d'abord créé la classe helper avec quelques méthodes pour traiter les énums. Une méthode obtient une description pour une valeur spécifique, l'autre méthode obtient toutes les valeurs et leurs descriptions pour un type.

public static class EnumHelper
{
  public static string Description(this Enum value)
  {
    var attributes = value.GetType().GetField(value.ToString()).GetCustomAttributes(typeof(DescriptionAttribute), false);
    if (attributes.Any())
      return (attributes.First() as DescriptionAttribute).Description;

    // If no description is found, the least we can do is replace underscores with spaces
    // You can add your own custom default formatting logic here
    TextInfo ti = CultureInfo.CurrentCulture.TextInfo;
    return ti.ToTitleCase(ti.ToLower(value.ToString().Replace("_", " ")));
  }

  public static IEnumerable<ValueDescription> GetAllValuesAndDescriptions(Type t)
  {
    if (!t.IsEnum)
      throw new ArgumentException($"{nameof(t)} must be an enum type");

    return Enum.GetValues(t).Cast<Enum>().Select((e) => new ValueDescription() { Value = e, Description = e.Description() }).ToList();
  }
}

ensuite, nous créons un ValueConverter . Hériter de MarkupExtension le rend plus facile à utiliser dans XAML donc nous n'avons pas à le déclarer comme une ressource.

[ValueConversion(typeof(Enum), typeof(IEnumerable<ValueDescription>))]
public class EnumToCollectionConverter : MarkupExtension, IValueConverter
{
  public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
  {
    return EnumHelper.GetAllValuesAndDescriptions(value.GetType());
  }
  public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
  {
    return null;
  }
  public override object ProvideValue(IServiceProvider serviceProvider)
  {
    return this;
  }
}

mon ViewModel n'a besoin que d'une propriété à laquelle mon View peut se rattacher pour les SelectedValue et ItemsSource du combobox:

private PlayerClass playerClass;

public PlayerClass SelectedClass
{
  get { return playerClass; }
  set
  {
    if (playerClass != value)
    {
      playerClass = value;
      OnPropertyChanged(nameof(SelectedClass));
    }
  }
}

et enfin de lier la vue ComboBox (en utilisant le ValueConverter dans le ItemsSource de liaison)...

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

pour mettre en œuvre cette solution, vous n'avez qu'à copier mes classes EnumHelper et EnumToCollectionConverter . Ils travailleront avec n'importe quel enums. En outre, Je ne l'ai pas inclus ici, mais la classe ValueDescription est juste une classe simple avec 2 propriétés d'objet public , un appelé Value , un appelé Description . Vous pouvez créer vous-même ou vous pouvez modifier le code pour utiliser un Tuple<object, object> ou KeyValuePair<object, object>

93
répondu Nick 2018-02-27 22:09:12

j'ai utilisé une autre solution en utilisant MarkupExtension.

  1. j'ai fait de la classe qui fournit des articles source:

    public class EnumToItemsSource : MarkupExtension
    {
        private readonly Type _type;
    
        public EnumToItemsSource(Type type)
        {
            _type = type;
        }
    
        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            return Enum.GetValues(_type)
                .Cast<object>()
                .Select(e => new { Value = (int)e, DisplayName = e.ToString() });
        }
    }
    
  2. c'est presque tout... Maintenant, utilisez-le dans XAML:

        <ComboBox DisplayMemberPath="DisplayName"
              ItemsSource="{persons:EnumToItemsSource {x:Type enums:States}}"
              SelectedValue="{Binding Path=WhereEverYouWant}"
              SelectedValuePath="Value" />
    
  3. changer "enums:States" à votre enum

41
répondu tom.maruska 2013-08-17 19:56:44

Use ObjectDataProvider:

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

et puis lier à la ressource statique:

ItemsSource="{Binding Source={StaticResource enumValues}}"
17
répondu druss 2017-05-27 16:55:11

la réponse de Nick m'a vraiment aidé, mais j'ai réalisé qu'il pouvait être légèrement modifié, pour éviter une classe supplémentaire, précieux description. Je me suis souvenu qu'il existe déjà une classe KeyValuePair dans le cadre, donc cela peut être utilisé à la place.

le code ne change que légèrement:

public static IEnumerable<KeyValuePair<string, string>> GetAllValuesAndDescriptions<TEnum>() where TEnum : struct, IConvertible, IComparable, IFormattable
    {
        if (!typeof(TEnum).IsEnum)
        {
            throw new ArgumentException("TEnum must be an Enumeration type");
        }

        return from e in Enum.GetValues(typeof(TEnum)).Cast<Enum>()
               select new KeyValuePair<string, string>(e.ToString(),  e.Description());
    }


public IEnumerable<KeyValuePair<string, string>> PlayerClassList
{
   get
   {
       return EnumHelper.GetAllValuesAndDescriptions<PlayerClass>();
   }
}

et enfin le XAML:

<ComboBox ItemSource="{Binding Path=PlayerClassList}"
          DisplayMemberPath="Value"
          SelectedValuePath="Key"
          SelectedValue="{Binding Path=SelectedClass}" />

j'espère que cela sera utile aux autres.

9
répondu Roger 2013-05-30 13:58:21

vous aurez besoin de créer un tableau des valeurs dans l'enum, qui peut être créé en appelant système.Enum.GetValues () , en lui passant le Type de l'enum dont vous voulez les éléments.

si vous spécifiez cela pour la propriété ItemsSource , alors elle doit être remplie avec toutes les valeurs de l'enum. Vous voulez probablement lier SelectedItem à EffectStyle (en supposant que c'est une propriété du même enum, et contient la valeur courante).

8
répondu Andy 2011-05-26 22:37:10

tous les messages ci-dessus ont manqué un tour simple. Il est possible, à partir de la liaison de SelectedValue, de trouver comment remplir la source Itemss de manière automatique afin que votre markup XAML soit juste.

<Controls:EnumComboBox SelectedValue="{Binding Fool}"/>

par exemple dans mon modèle j'ai

public enum FoolEnum
    {
        AAA, BBB, CCC, DDD

    };


    FoolEnum _Fool;
    public FoolEnum Fool
    {
        get { return _Fool; }
        set { ValidateRaiseAndSetIfChanged(ref _Fool, value); }
    }

ValidateRaiseAndSetIfChanged est mon crochet INPC. Le vôtre peut différer.

la mise en œuvre D'EnumComboBox est comme suit, mais d'abord je vais avoir besoin d'un peu d'aide pour obtenir mes chaînes d'énumération et les valeurs

    public static List<Tuple<object, string, int>> EnumToList(Type t)
    {
        return Enum
            .GetValues(t)
            .Cast<object>()
            .Select(x=>Tuple.Create(x, x.ToString(), (int)x))
            .ToList();
    }

et la classe principale ( notez que J'utilise ReactiveUI pour accrocher les changements de propriété via WhenAny)

using ReactiveUI;
using ReactiveUI.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Documents;

namespace My.Controls
{
    public class EnumComboBox : System.Windows.Controls.ComboBox
    {
        static EnumComboBox()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(EnumComboBox), new FrameworkPropertyMetadata(typeof(EnumComboBox)));
        }

        protected override void OnInitialized( EventArgs e )
        {
            base.OnInitialized(e);

            this.WhenAnyValue(p => p.SelectedValue)
                .Where(p => p != null)
                .Select(o => o.GetType())
                .Where(t => t.IsEnum)
                .DistinctUntilChanged()
                .ObserveOn(RxApp.MainThreadScheduler)
                .Subscribe(FillItems);
        }

        private void FillItems(Type enumType)
        {
            List<KeyValuePair<object, string>> values = new List<KeyValuePair<object,string>>();

            foreach (var idx in EnumUtils.EnumToList(enumType))
            {
                values.Add(new KeyValuePair<object, string>(idx.Item1, idx.Item2));
            }

            this.ItemsSource = values.Select(o=>o.Key.ToString()).ToList();

            UpdateLayout();
            this.ItemsSource = values;
            this.DisplayMemberPath = "Value";
            this.SelectedValuePath = "Key";

        }
    }
}

vous devez également définir le style correctement dans Generic.XAML ou votre boite ne rendra rien et vous arracherez vos cheveux.

<Style TargetType="{x:Type local:EnumComboBox}" BasedOn="{StaticResource {x:Type ComboBox}}">
</Style>

et c'est tout. Cela pourrait évidemment être étendu pour soutenir i18n mais rendrait le poste plus long.

4
répondu bradgonesurfing 2013-09-17 12:51:16
public class EnumItemsConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (!value.GetType().IsEnum)
            return false;

        var enumName = value.GetType();
        var obj = Enum.Parse(enumName, value.ToString());

        return System.Convert.ToInt32(obj);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return Enum.ToObject(targetType, System.Convert.ToInt32(value));
    }
}

vous devriez étendre la réponse de Rogers et Greg avec une telle sorte de convertisseur de valeur Enum, si vous liez directement aux propriétés de modèle d'objet enum.

2
répondu Ruberoid 2014-11-08 16:30:06
Les applications universelles

semblent fonctionner un peu différemment; elles n'ont pas toute la puissance de XAML complet. Ce qui a fonctionné pour moi est:

  1. j'ai créé une liste des valeurs enum comme les enum (non converti en strings ou à des entiers) et lié La Source D'item ComboBox à celle
  2. alors je pourrais lier L'article de ComboBox choisi à ma propriété publique dont le type est l'enum en question

Juste pour le plaisir, je il a créé une petite classe de modèles pour aider à cela et l'a publié dans les MSDN Samples pages . L'extra laissez-moi éventuellement remplacer les noms des enums et à me cacher certaines énumérations. Mon code ressemble terriblement à celui de Nick (ci-dessus), que j'aurais aimé voir plus tôt.

Running the sample; it includes multiple twoway bindings to the enum

2
répondu rsclient 2016-01-26 16:48:42

si vous êtes lié à une propriété enum réelle sur votre ViewModel, pas à une représentation int d'un enum, les choses deviennent délicates. J'ai trouvé qu'il est nécessaire de se lier à la représentation string, pas à la valeur int comme on l'attend dans tous les exemples ci-dessus.

vous pouvez dire si c'est le cas en liant une simple boîte de texte à la propriété à laquelle vous voulez lier sur votre ViewModel. S'il affiche du texte, liez-le à la chaîne. S'il affiche un nombre, liez-le à la valeur. Remarque j'ai utilisé affichage deux fois qui serait normalement une erreur, mais c'est la seule façon qu'il fonctionne.

<ComboBox SelectedValue="{Binding ElementMap.EdiDataType, Mode=TwoWay}"
                      DisplayMemberPath="Display"
                      SelectedValuePath="Display"
                      ItemsSource="{Binding Source={core:EnumToItemsSource {x:Type edi:EdiDataType}}}" />

Greg

1
répondu Greg0 2013-08-17 20:06:55

j'ai aimé tom.réponse de maruska , mais j'avais besoin de supporter n'importe quel type d'enum que mon modèle pourrait rencontrer à l'exécution. Pour cela, j'ai dû utiliser une liaison pour spécifier le type de l'extension markup. J'ai pu travailler dans cette réponse de nicolay.anykienko à venir avec une extension de balisage très flexible qui fonctionnerait dans tous les cas je peux penser à. Il est consommé comme ceci:

<ComboBox SelectedValue="{Binding MyEnumProperty}" 
          SelectedValuePath="Value"
          ItemsSource="{local:EnumToObjectArray SourceEnum={Binding MyEnumProperty}}" 
          DisplayMemberPath="DisplayName" />

la source pour le purée jusqu'extension de balisage référencé ci-dessus:

class EnumToObjectArray : MarkupExtension
{
    public BindingBase SourceEnum { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        IProvideValueTarget target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
        DependencyObject targetObject;
        DependencyProperty targetProperty;

        if (target != null && target.TargetObject is DependencyObject && target.TargetProperty is DependencyProperty)
        {
            targetObject = (DependencyObject)target.TargetObject;
            targetProperty = (DependencyProperty)target.TargetProperty;
        }
        else
        {
            return this;
        }

        BindingOperations.SetBinding(targetObject, EnumToObjectArray.SourceEnumBindingSinkProperty, SourceEnum);

        var type = targetObject.GetValue(SourceEnumBindingSinkProperty).GetType();

        if (type.BaseType != typeof(System.Enum)) return this;

        return Enum.GetValues(type)
            .Cast<Enum>()
            .Select(e => new { Value=e, Name = e.ToString(), DisplayName = Description(e) });
    }

    private static DependencyProperty SourceEnumBindingSinkProperty = DependencyProperty.RegisterAttached("SourceEnumBindingSink", typeof(Enum)
                       , typeof(EnumToObjectArray), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits));

    /// <summary>
    /// Extension method which returns the string specified in the Description attribute, if any.  Oherwise, name is returned.
    /// </summary>
    /// <param name="value">The enum value.</param>
    /// <returns></returns>
    public static string Description(Enum value)
    {
        var attrs = value.GetType().GetField(value.ToString()).GetCustomAttributes(typeof(DescriptionAttribute), false);
        if (attrs.Any())
            return (attrs.First() as DescriptionAttribute).Description;

        //Fallback
        return value.ToString().Replace("_", " ");
    }
}
1
répondu Hamish 2017-05-23 12:34:20

il y a beaucoup d'excellentes réponses à cette question et je soumets humblement la mienne. Je trouve que le mien est un peu plus simple et plus élégante. Il ne nécessite qu'un convertisseur de valeurs.

donné enum...

public enum ImageFormat
{
    [Description("Windows Bitmap")]
    BMP,
    [Description("Graphics Interchange Format")]
    GIF,
    [Description("Joint Photographic Experts Group Format")]
    JPG,
    [Description("Portable Network Graphics Format")]
    PNG,
    [Description("Tagged Image Format")]
    TIFF,
    [Description("Windows Media Photo Format")]
    WDP
}

et un convertisseur de valeurs...

public class ImageFormatValueConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is ImageFormat format)
        {
            return GetString(format);
        }

        return null;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is string s)
        {
            return Enum.Parse(typeof(ImageFormat), s.Substring(0, s.IndexOf(':')));
        }
        return null;
    }

    public string[] Strings => GetStrings();

    public static string GetString(ImageFormat format)
    {
        return format.ToString() + ": " + GetDescription(format);
    }

    public static string GetDescription(ImageFormat format)
    {
        return format.GetType().GetMember(format.ToString())[0].GetCustomAttribute<DescriptionAttribute>().Description;

    }
    public static string[] GetStrings()
    {
        List<string> list = new List<string>();
        foreach (ImageFormat format in Enum.GetValues(typeof(ImageFormat)))
        {
            list.Add(GetString(format));
        }

        return list.ToArray();
    }
}

des ressources...

    <local:ImageFormatValueConverter x:Key="ImageFormatValueConverter"/>

XAML declaration...

    <ComboBox Grid.Row="9" ItemsSource="{Binding Source={StaticResource ImageFormatValueConverter}, Path=Strings}"
              SelectedItem="{Binding Format, Converter={StaticResource ImageFormatValueConverter}}"/>

voir modèle...

    private ImageFormat _imageFormat = ImageFormat.JPG;
    public ImageFormat Format
    {
        get => _imageFormat;
        set
        {
            if (_imageFormat != value)
            {
                _imageFormat = value;
                OnPropertyChanged();
            }
        }
    }

résultat combobox...

ComboBox bound to enum

1
répondu AQuirky 2018-07-28 19:29:15

en utilisant ReactiveUI , j'ai créé la solution alternative suivante. Ce n'est pas une solution élégante tout-en-un, mais je pense qu'au moins c'est lisible.

dans mon cas, lier une liste de enum à un contrôle est un cas rare, donc je n'ai pas besoin de mettre la solution à l'échelle de la base de code. Toutefois, le code peut être rendu plus générique en remplaçant EffectStyleLookup.Item par Object . Je l'ai testé avec mon code, aucune modification n'est nécessaire. Qui signifie que la classe one helper peut être appliquée à n'importe quelle liste enum . Bien que cela réduirait sa lisibilité - ReactiveList<EnumLookupHelper> n'a pas un grand anneau à elle.

utilisant la classe helper suivante:

public class EffectStyleLookup
{
    public EffectStyle Item { get; set; }
    public string Display { get; set; }
}

dans le ViewModel, convertissez la liste des énums et exposez-la comme une propriété:

public ViewModel : ReactiveObject
{
  private ReactiveList<EffectStyleLookup> _effectStyles;
  public ReactiveList<EffectStyleLookup> EffectStyles
  {
    get { return _effectStyles; }
    set { this.RaiseAndSetIfChanged(ref _effectStyles, value); }
  }

  // See below for more on this
  private EffectStyle _selectedEffectStyle;
  public EffectStyle SelectedEffectStyle
  {
    get { return _selectedEffectStyle; }
    set { this.RaiseAndSetIfChanged(ref _selectedEffectStyle, value); }
  }

  public ViewModel() 
  {
    // Convert a list of enums into a ReactiveList
    var list = (IList<EffectStyle>)Enum.GetValues(typeof(EffectStyle))
      .Select( x => new EffectStyleLookup() { 
        Item = x, 
        Display = x.ToString()
      });

    EffectStyles = new ReactiveList<EffectStyle>( list );
  }
}

dans le ComboBox , utilisez la propriété SelectedValuePath , pour lier à la valeur originale enum :

<ComboBox Name="EffectStyle" DisplayMemberPath="Display" SelectedValuePath="Item" />

dans la vue, cela nous permet de lier l'original enum au SelectedEffectStyle dans la vue, mais Afficher la valeur ToString() dans le ComboBox :

this.WhenActivated( d =>
{
  d( this.OneWayBind(ViewModel, vm => vm.EffectStyles, v => v.EffectStyle.ItemsSource) );
  d( this.Bind(ViewModel, vm => vm.SelectedEffectStyle, v => v.EffectStyle.SelectedValue) );
});
0
répondu Mitkins 2017-10-12 00:46:49

j'ajoute mon commentaire (en VB, malheureusement, mais le concept peut être facilement répliqué en C# en un battement de coeur), parce que j'ai juste eu à faire référence à cela et je n'ai pas aimé aucune des réponses car ils étaient trop complexes. Cela ne devrait pas être difficile.

donc j'ai trouvé un moyen plus facile. Relier les recenseurs à un dictionnaire. Liez ce dictionnaire au Combobox.

Ma zone de liste déroulante:

<ComboBox x:Name="cmbRole" VerticalAlignment="Stretch" IsEditable="False" Padding="2" 
    Margin="0" FontSize="11" HorizontalAlignment="Stretch" TabIndex="104" 
    SelectedValuePath="Key" DisplayMemberPath="Value" />

mon code derrière. Espérons que cela aide quelqu'un d'autre.

Dim tDict As New Dictionary(Of Integer, String)
Dim types = [Enum].GetValues(GetType(Helper.Enumerators.AllowedType))
For Each x As Helper.Enumerators.AllowedType In types
    Dim z = x.ToString()
    Dim y = CInt(x)
    tDict.Add(y, z)
Next

cmbRole.ClearValue(ItemsControl.ItemsSourceProperty)
cmbRole.ItemsSource = tDict
0
répondu Laki Politis 2018-02-26 21:50:05

explication Simple et claire: http://brianlagunas.com/a-better-way-to-data-bind-enums-in-wpf /

xmlns:local="clr-namespace:BindingEnums"
xmlns:sys="clr-namespace:System;assembly=mscorlib"

...

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

...

<Grid>
    <ComboBox HorizontalAlignment="Center" VerticalAlignment="Center" MinWidth="150"
              ItemsSource="{Binding Source={StaticResource dataFromEnum}}"/>
</Grid>
0
répondu jlo-gmail 2018-07-17 21:18:52