Esiste un modo efficace per creare forme complesse?

5

Durante la creazione di un modulo di "ricerca" ho dovuto affrontare una complessità travolgente e mi chiedevo se esiste un modo ben noto per svolgere tale compito.

I campi del modulo in questione possono essere visualizzati qui: link

Qui ci sono alcuni problemi che ho dovuto affrontare e ho cercato di risolvere con soluzioni semplici, anche se alla fine ha comportato un sacco di copia e incolla:

  • Quando l'utente può selezionare più scelte per un campo o no, ho sistematicamente dovuto creare una raccolta di esse, aggiungendo un valore "null" per la capacità di ignorare quel campo. Inoltre, poiché si tratta di valori di enumerazione, non sono user-friendly da leggere e, pertanto, è necessario un campo di descrizione aggiuntivo che verrà visualizzato sull'interfaccia utente. E alla fine tutti questi valori user-friendly devono essere riconvertiti all'oggetto originale.

  • Selezione effettuata dall'utente, queste scelte devono essere recuperate e per questo, purtroppo nel mio caso il binding dei dati WPF ha mostrato i suoi limiti. Esempio: quando un utente sceglie elementi da una collezione, non c'è modo di aggiornare l'oggetto di destinazione con queste selezioni. (Ovviamente questo non è realistico in quanto l'oggetto funge da origine dati)

Alla fine riprende un sacco di recupero manuale da caselle combinate, caselle di elenco, ecc ... un oggetto middle-man per ricevere tutto questo e per questo è necessario aggiungere anche la convalida .

Una strategia diventa quindi imperativa poiché dovrò realizzare un paio di queste forme complesse.

Ho cercato sul Web soluzioni ma accanto a esempi estremamente semplici non ho trovato nulla di rilevante.

Quindi la domanda è: esiste un modo efficace che aiuti nella creazione di moduli utente complessi?

    
posta Aybe 03.07.2013 - 20:02
fonte

1 risposta

7

L'ho risolto e alla fine è andato tutto bene, quindi darò qui le mie raccomandazioni in base a questa dolorosa esperienza.

(questi suggerimenti si applicano a WPF)

  • Convalida: usa INotifyDataErrorInfo al posto delle regole in XAML, la validazione avviene invece nell'oggetto e anche se è complicato configurarla, ne vale davvero la pena. Questa interfaccia è appropriata per la convalida di un oggetto complesso. Ecco un eccellente articolo che lo mostra: link Ti suggerisco di non copiare / incollare ma scriverlo manualmente. (L'ho fatto e mi ci è voluto meno tempo che copiare / incollare e cercare di capire cosa farà dopo)

  • Aggiornamento dell'oggetto ogni volta che l'utente immette i valori: è chiaramente meglio non fare nulla durante gli eventi di controllo come TextChanged, ma attenersi alla convalida come menzionato sopra. Hook to Validation.Error eventi dei tuoi controlli per la gestione della logica della tua API (nel mio caso quando 'nome' è impostato l'utente non può cercare per 'familiarità' quindi ho disabilitato i controlli appropriati).

  • Questo è il simbolo di un pulsante di ricerca che viene abilitato solo quando tutti i valori sono corretti e questo è in realtà il luogo in cui devi prendere tutti i parametri e le tue cose, tutto in un posto, non sparpagliato più attraverso gli eventi dei controlli .

  • Inoltre, non esitare a utilizzare un oggetto middle-man anche se non conterrà tutti i campi dell'oggetto originale che si sono dimostrati utili, poiché è lì che ho implementato INotifyDataDataErrorInfo. Non c'era assolutamente alcun modo per modificare l'oggetto sorgente che va bene e parte di una libreria comunque! Per più scelte che un utente può fare su un ListBox, non l'ho usato, le ho solo popolate e basta. (sono tutti validi per natura, nel mio caso)

Ora alcuni screenshot e spiegazioni.

Ecco l'API, non su 2 o 3 campi, ma 21, quando l'ho visto per la prima volta sapevo che sarebbe stato difficile implementarlo, ma lo sottovalutavo ancora ...

Qui il pulsante di ricerca viene abilitato solo quando la convalida è corretta,

Usandounmetodoestremamentesemplice,fortunatamentenelmiocasoeranonecessarisoloquesti3casi.

private void ToggleSearch() { var b1 = GetHasErrorPropertyValue(TextBoxArtistLocation); var b2 = GetHasErrorPropertyValue(TextBoxDescription); var b3 = GetHasErrorPropertyValue(TextBoxName); ButtonSearch.IsEnabled = !(b1 || b2 || b3); } private static bool GetHasErrorPropertyValue(DependencyObject dependencyObject) { if (dependencyObject == null) throw new ArgumentNullException("dependencyObject"); return (bool)dependencyObject.GetValue(Validation.HasErrorProperty); } // Hooking to Validation.Error attached property, these boxes all point to here. private void TextBox_OnError(object sender, ValidationErrorEventArgs e) { ToggleSearch(); }

Ecco l'output della query, l'unica cosa che preoccupa rispetto alla versione precedente è che per il test devo cercare ogni volta per vedere il risultato della query costruita per cui ho assegnato Alt-S come time-saver. In precedenza, stavo recuperando il risultato su ogni valore modificato in ciascun controllo, mentre sembrava semplice poiché si trattava di metodi a 5 righe che presto si trasformarono in un incubo. Con il nuovo metodo, ho una mano che imposta i campi con il mouse, l'altra premendo Alt-S e ovviamente una classe molto più semplice per capire / use / debug.

Ultimoschermo:comepuoivedere,nonc'èmoltoinesso,i3metodimigliorichiamanociascunounodeimetodiseguenti,pensochesianoauto-esplicativi.Imetodihelpersonousatiperimpostare/recuperarevaloridaicontrollieperformarelaquery.

Ultimi due campioni:

Popolazione dei dati:

    private async Task PopulateData()
    {
        string apiKey = App.GetApiKey();

        var years = GetDescribedEnum<ArtistSearchYear>().ToList();
        ComboBoxArtistEndYearAfter.ItemsSource = years;
        ComboBoxArtistEndYearBefore.ItemsSource = years;
        ComboBoxArtistStartYearAfter.ItemsSource = years;
        ComboBoxArtistStartYearBefore.ItemsSource = years;

        var rankTypes = GetDescribedEnum<ArtistSearchRankType>().ToList();
        ComboBoxRankType.ItemsSource = rankTypes;

        var results = Enumerable.Range(0, 101).Select(s => s.ToString(CultureInfo.InvariantCulture)).ToList();
        results.Insert(0, string.Empty);
        ComboBoxResults.ItemsSource = results;

        var sort = GetDescribedEnum<ArtistSearchSort>().OrderBy(s => s.Description).ToList();
        ComboBoxSort.ItemsSource = sort;

        var start = Enumerable.Range(0, 3).Select(s => (s * 15).ToString(CultureInfo.InvariantCulture)).ToList();
        start.Insert(0, string.Empty);
        ComboBoxStart.ItemsSource = start;

        var buckets = GetDescribedEnum<ArtistSearchBucket>().OrderBy(s => s.Description).ToList();
        ListBoxBuckets.ItemsSource = buckets;

        var genres = await Queries.ArtistListGenres(apiKey);
        ListBoxGenre.ItemsSource = genres.Genres.Select(s => s.Name);

        var styles = await Queries.ArtistListTerms(apiKey, ArtistListTermsType.Style);
        ListBoxStyle.ItemsSource = styles.Terms.Select(s => s.Name);

        var moods = await Queries.ArtistListTerms(apiKey, ArtistListTermsType.Mood);
        ListBoxMood.ItemsSource = moods.Terms.Select(s => s.Name);
    }

Recupero dei parametri:

    private void RunSearch()
    {
        var parameters = new ArtistSearchParameters();

        var year = (Func<ComboBox, ArtistSearchYear?>)((c) =>
            {
                if (c.SelectedValue == null) return null;
                var value = GetDescribedObjectValue<ArtistSearchYear>(c.SelectedValue);
                return value == ArtistSearchYear.Invalid ? (ArtistSearchYear?)null : value;
            });
        parameters.ArtistEndYearAfter = year(ComboBoxArtistEndYearAfter);
        parameters.ArtistEndYearBefore = year(ComboBoxArtistEndYearBefore);
        parameters.ArtistStartYearAfter = year(ComboBoxArtistStartYearAfter);
        parameters.ArtistStartYearBefore = year(ComboBoxArtistStartYearBefore);

        if (ComboBoxRankType.SelectedValue == null)
        {
            parameters.RankType = null;
        }
        else
        {
            var value = GetDescribedObjectValue<ArtistSearchRankType>(ComboBoxRankType.SelectedValue);
            parameters.RankType = value == ArtistSearchRankType.Invalid ? (ArtistSearchRankType?)null : value;
        }

        var selectedValue = ComboBoxSort.SelectedValue;
        if (selectedValue == null)
        {
            parameters.Sort = null;
        }
        else
        {
            var value = GetDescribedObjectValue<ArtistSearchSort>(selectedValue);
            parameters.Sort = value == ArtistSearchSort.Invalid ? (ArtistSearchSort?)null : value;
        }

        parameters.Start = StringToNullableInt(GetSelectorValue<string>(ComboBoxStart));
        parameters.Results = StringToNullableInt(GetSelectorValue<string>(ComboBoxResults));

        parameters.MaxFamiliarity = SliderMaxFamiliarity.Value < 1.0d ? (double?)SliderMaxFamiliarity.Value : null;
        parameters.MinFamiliarity = SliderMinFamiliarity.Value > 0.0d ? (double?)SliderMinFamiliarity.Value : null;
        parameters.MaxHotttnesss = SliderMaxHottness.Value < 1.0d ? (double?)SliderMaxHottness.Value : null;
        parameters.MinHotttnesss = SliderMinHottness.Value > 0.0d ? (double?)SliderMinHottness.Value : null;

        if (CheckBoxFuzzyMatch.IsChecked.HasValue)
            parameters.FuzzyMatch = CheckBoxFuzzyMatch.IsChecked.Value ? (bool?)true : null;
        if (CheckBoxLimit.IsChecked.HasValue)
            parameters.Limit = CheckBoxLimit.IsChecked.Value ? (bool?)true : null;

        var buckets =
            GetSelectedItems<DescribedObject<ArtistSearchBucket>>(ListBoxBuckets)
                          .OrderBy(s => s.Description)
                          .Select(s => s.Value)
                          .ToList();
        parameters.Buckets = buckets.Count > 0 ? buckets : null;
        var genres = GetSelectedItems<string>(ListBoxGenre).OrderBy(s => s).ToList();
        parameters.Genres = genres.Count > 0 ? genres : null;
        var moods = GetSelectedItems<string>(ListBoxMood).OrderBy(s => s).ToList();
        parameters.Moods = moods.Count > 0 ? moods : null;
        var style = GetSelectedItems<string>(ListBoxStyle).OrderBy(s => s).ToList();
        parameters.Styles = style.Count > 0 ? style : null;

        parameters.ArtistLocation = string.IsNullOrEmpty(TextBoxArtistLocation.Text)
                                        ? null
                                        : TextBoxArtistLocation.Text;
        parameters.Description = string.IsNullOrEmpty(TextBoxDescription.Text)
                                     ? null
                                     : Regex.Split(TextBoxDescription.Text, @", ?")
                                            .Where(s => !string.IsNullOrWhiteSpace(s))
                                            .ToList();
        if (string.IsNullOrEmpty(TextBoxName.Text))
        {
            parameters.Name = null;
        }
        else
        {
            parameters.Name = TextBoxName.Text;
            parameters.MaxFamiliarity = null;
            parameters.MaxHotttnesss = null;
            parameters.MinFamiliarity = null;
            parameters.MinHotttnesss = null;
        }

    }

Nel mio caso i valori nulli sono stati fondamentali in quanto mi hanno permesso di scartare i valori durante la creazione della query.

Per costruire la query ho imbrogliato in qualche modo usando JSON:

    protected static string GetUrlParameters(string json)
    {
        if (json == null) throw new ArgumentNullException("json");
        JsonTextReader reader = new JsonTextReader(new StringReader(json));
        string path = string.Empty;
        List<string> list = new List<string>();
        while (reader.Read())
        {
            JsonToken type = reader.TokenType;
            if (type == JsonToken.PropertyName)
            {
                path = reader.Path;
            }
            else
            {
                bool b0 = type == JsonToken.Integer;
                bool b1 = type == JsonToken.Float;
                bool b2 = type == JsonToken.String;
                bool b3 = type == JsonToken.Boolean;
                if (b0 || b1 || b2 || b3)
                {
                    var value = reader.Value.ToString();
                    if (b3) value = value.ToLower();

                    string item = string.Format("{0}={1}", path, value);
                    list.Add(item);
                }
            }
        }
        return string.Join("&", list);
    }

DescribedObject è un oggetto middle-man che contiene il valore desiderato e la descrizione viene recuperata da un valore [DescriptionAttribute]

public class DescribedObject 
{
    private readonly string _description;
    private readonly Object _value;

    public DescribedObject() // NOTE : for design-time
    {

    }
    protected DescribedObject(Object value, string description)
    {
        _value = value;
        _description = description;
    }

    public string Description
    {
        get { return _description; }
    }

    public Object Value
    {
        get { return _value; }
    }

    public override string ToString()
    {
        return string.Format("Value: {0}, Description: {1}", _value, _description);
    }
}

public class DescribedObject<T> : DescribedObject
{

    public DescribedObject(T value, string description)
        : base(value, description)
    {
    }
    public new T Value
    {
        get
        {
            return (T)base.Value;
        }
    }
}

Un oggetto descritto:

[JsonConverter(typeof(PropertyEnumConverter))]
public enum ArtistSearchYear
{
    [Description(null), JsonProperty(null)]
    Invalid = 0,
    [Description("1970"), JsonProperty("1970")]
    Year1970,
    [Description("1971"), JsonProperty("1971")]
    Year1971
}

(PropertyEnumConverter è un oggetto semplice che converte questi valori in base all'attributo JsonProperty.)

Quindi, come puoi vedere, l'ho tenuto stupidamente semplice, ma mi ci è voluto un po 'di tempo perché inizialmente avevo un approccio più complesso, anche in questo caso la semplicità si è rivelata davvero utile anche se a volte sembra stupida.

Non ho parlato del lato XAML, brevemente non c'è assolutamente nulla al suo interno accanto a controlli e modelli, solo i binding hanno queste due proprietà impostate per la convalida. (inoltre, l'oggetto implementa INotifyPropertyChanged)

<TextBox x:Name="TextBoxArtistLocation"
            Style="{StaticResource TextBoxInError}"
            Validation.Error="TextBox_OnError"
            Text="{Binding ArtistLocation, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged}" />

Spero che questa risposta aiuti qualcuno, ho cercato di essere il più conciso possibile, ma se hai domande / commenti, chiedi qui sotto.

    
risposta data 05.07.2013 - 16:04
fonte

Leggi altre domande sui tag