Is there an effective way for creating complex forms?

While creating a ‘search’ form I’ve been faced to an overwhelming complexity and I was wondering whether there is a well-known way to accomplish such task.

Fields of the form in question can be seen here: http://developer.echonest.com/docs/v4/artist.html#search

Here are a few issues I’ve been faced to and tried to solve with simple solutions though it led to a lot of copying and pasting in the end :

  • When the user can select multiple choices for a field or not, I systematically had to create a collection of them, adding a ‘null’ value for the ability of ignoring that field. Also, as these are enumeration values they are not user-friendly to read and therefore needed an extra description field that will be shown instead on the UI. And at the end all this user-friendly values need to be converted back to the original object.

  • Selection being made by the user, these choices need to be retrieved and for this, unfortunately in my case WPF data binding shown its limits. Example : when a user chooses items from a collection, there is no way to update the target object with these selections. (Obviously this is unrealistic as the object serves as a data source)

In the end it resumes to a lot of manual fetching from combo boxes, list boxes etc … a middle-man object to receive all that and to this you have to add validation as well.

A strategy therefore becomes imperative as I will have to realise a couple of these complex forms.

I’ve been searching the web for solutions but beside extremely simple examples I found nothing relevant.

So the question is, is there an effective way that helps when creating complex user forms ?

7

I’ve sorted it out and in the end it went pretty well so I’ll give here my recommendations according this painful experience.

(these tips apply for WPF)

  • Validation : use INotifyDataErrorInfo instead of rules in XAML, your validation takes place in the object instead and even it’s sort of complicate to setup it is really worth it. This interface is appropriate for validation of a complex object. Here’s an excellent article showing it : http://anthymecaillard.wordpress.com/2012/03/26/wpf-4-5-validation-asynchrone/ I’d suggest you to not copy/paste it but write it manually. (I did and it took me less time than copy/paste and trying to grasp what it does later)

  • Updating your object whenever user inputs values : it’s clearly better to do nothing during controls events such as TextChanged but to stick to validation as mentioned above. Hook to Validation.Error events of your controls for managing the logic of your API (in my case when ‘name’ is set the user cannot search by ‘familiarity’ so I disabled appropriate controls).

  • That leds to a search button that gets enabled only when all values are correct and this is actually the place where you’d grab all params and your stuff, all in one place, not scattered anymore through events of controls.

  • Also, do not hesitate to use a middle-man object even though it won’t contain all the fields of the original object it still proved to be helpful as that’s where I implemented INotifyDataDataErrorInfo. There was absolutely no way that I modify the source object which is fine and part of a library anyway ! For multiple choices a user can do on a ListBox, I didn’t use it, I just populated them and that’s it. (they are all valid by nature, in my case)

Now a few screenshots and explanations.

Here’s the API, not about 2 or 3 fields but 21, when I saw it first I knew it would be a pain to implement but still I underestimated it …

Here the search button gets enabled only when validation is correct,

Using an extremely simple method, luckily in my case only these 3 cases were needed.

    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();
    }

Here’s the output of the query, well the only bothering stuff compared to previous version is that for testing I have to search every time to see the result of the built query for which I assigned Alt-S as a time-saver. Previously I was fetching the result on each value changed in each control, while it looked simple as these were 5 lines methods it soon went to a nightmare. With the new method, I have one hand setting the fields with mouse the other pressing Alt-S and obviously a much simpler class to understand/use/debug.

Last screen : as you can see there isn’t much in it, the 3 top methods each call one of the methods below, I think they are self-explanatory. The helper methods are used for setting/retrieving values from controls and to form the query.

Last two samples :

Populating data :

    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);
    }

Retrieving parameters :

    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;
        }

    }

In my case nullables were vital as they allowed me to discard values when building the query.

For building the query I cheated somehow using 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 is a middle-man object that holds the desired value and the description is fetched from a value [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;
        }
    }
}

A described object :

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

(PropertyEnumConverter is a simple object that will convert these values according their JsonProperty attribute.)

So as you can see, I kept it stupidly simple but it took me some time as at first I had a more complex approach, again the simplicity proven really helpful though it sounds stupid sometimes.

I haven’t talked about the XAML side, briefly there’s absolutely nothing in it beside controls and templates, only bindings have these two properties set for them to validate. (also, the object implements INotifyPropertyChanged)

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

I hope this answer will help someone, I’ve tried to be as concise as possible but if you have any question/comment, ask below.

2

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa

Is there an effective way for creating complex forms?

While creating a ‘search’ form I’ve been faced to an overwhelming complexity and I was wondering whether there is a well-known way to accomplish such task.

Fields of the form in question can be seen here: http://developer.echonest.com/docs/v4/artist.html#search

Here are a few issues I’ve been faced to and tried to solve with simple solutions though it led to a lot of copying and pasting in the end :

  • When the user can select multiple choices for a field or not, I systematically had to create a collection of them, adding a ‘null’ value for the ability of ignoring that field. Also, as these are enumeration values they are not user-friendly to read and therefore needed an extra description field that will be shown instead on the UI. And at the end all this user-friendly values need to be converted back to the original object.

  • Selection being made by the user, these choices need to be retrieved and for this, unfortunately in my case WPF data binding shown its limits. Example : when a user chooses items from a collection, there is no way to update the target object with these selections. (Obviously this is unrealistic as the object serves as a data source)

In the end it resumes to a lot of manual fetching from combo boxes, list boxes etc … a middle-man object to receive all that and to this you have to add validation as well.

A strategy therefore becomes imperative as I will have to realise a couple of these complex forms.

I’ve been searching the web for solutions but beside extremely simple examples I found nothing relevant.

So the question is, is there an effective way that helps when creating complex user forms ?

7

I’ve sorted it out and in the end it went pretty well so I’ll give here my recommendations according this painful experience.

(these tips apply for WPF)

  • Validation : use INotifyDataErrorInfo instead of rules in XAML, your validation takes place in the object instead and even it’s sort of complicate to setup it is really worth it. This interface is appropriate for validation of a complex object. Here’s an excellent article showing it : http://anthymecaillard.wordpress.com/2012/03/26/wpf-4-5-validation-asynchrone/ I’d suggest you to not copy/paste it but write it manually. (I did and it took me less time than copy/paste and trying to grasp what it does later)

  • Updating your object whenever user inputs values : it’s clearly better to do nothing during controls events such as TextChanged but to stick to validation as mentioned above. Hook to Validation.Error events of your controls for managing the logic of your API (in my case when ‘name’ is set the user cannot search by ‘familiarity’ so I disabled appropriate controls).

  • That leds to a search button that gets enabled only when all values are correct and this is actually the place where you’d grab all params and your stuff, all in one place, not scattered anymore through events of controls.

  • Also, do not hesitate to use a middle-man object even though it won’t contain all the fields of the original object it still proved to be helpful as that’s where I implemented INotifyDataDataErrorInfo. There was absolutely no way that I modify the source object which is fine and part of a library anyway ! For multiple choices a user can do on a ListBox, I didn’t use it, I just populated them and that’s it. (they are all valid by nature, in my case)

Now a few screenshots and explanations.

Here’s the API, not about 2 or 3 fields but 21, when I saw it first I knew it would be a pain to implement but still I underestimated it …

Here the search button gets enabled only when validation is correct,

Using an extremely simple method, luckily in my case only these 3 cases were needed.

    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();
    }

Here’s the output of the query, well the only bothering stuff compared to previous version is that for testing I have to search every time to see the result of the built query for which I assigned Alt-S as a time-saver. Previously I was fetching the result on each value changed in each control, while it looked simple as these were 5 lines methods it soon went to a nightmare. With the new method, I have one hand setting the fields with mouse the other pressing Alt-S and obviously a much simpler class to understand/use/debug.

Last screen : as you can see there isn’t much in it, the 3 top methods each call one of the methods below, I think they are self-explanatory. The helper methods are used for setting/retrieving values from controls and to form the query.

Last two samples :

Populating data :

    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);
    }

Retrieving parameters :

    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;
        }

    }

In my case nullables were vital as they allowed me to discard values when building the query.

For building the query I cheated somehow using 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 is a middle-man object that holds the desired value and the description is fetched from a value [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;
        }
    }
}

A described object :

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

(PropertyEnumConverter is a simple object that will convert these values according their JsonProperty attribute.)

So as you can see, I kept it stupidly simple but it took me some time as at first I had a more complex approach, again the simplicity proven really helpful though it sounds stupid sometimes.

I haven’t talked about the XAML side, briefly there’s absolutely nothing in it beside controls and templates, only bindings have these two properties set for them to validate. (also, the object implements INotifyPropertyChanged)

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

I hope this answer will help someone, I’ve tried to be as concise as possible but if you have any question/comment, ask below.

2

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa

Is there an effective way for creating complex forms?

While creating a ‘search’ form I’ve been faced to an overwhelming complexity and I was wondering whether there is a well-known way to accomplish such task.

Fields of the form in question can be seen here: http://developer.echonest.com/docs/v4/artist.html#search

Here are a few issues I’ve been faced to and tried to solve with simple solutions though it led to a lot of copying and pasting in the end :

  • When the user can select multiple choices for a field or not, I systematically had to create a collection of them, adding a ‘null’ value for the ability of ignoring that field. Also, as these are enumeration values they are not user-friendly to read and therefore needed an extra description field that will be shown instead on the UI. And at the end all this user-friendly values need to be converted back to the original object.

  • Selection being made by the user, these choices need to be retrieved and for this, unfortunately in my case WPF data binding shown its limits. Example : when a user chooses items from a collection, there is no way to update the target object with these selections. (Obviously this is unrealistic as the object serves as a data source)

In the end it resumes to a lot of manual fetching from combo boxes, list boxes etc … a middle-man object to receive all that and to this you have to add validation as well.

A strategy therefore becomes imperative as I will have to realise a couple of these complex forms.

I’ve been searching the web for solutions but beside extremely simple examples I found nothing relevant.

So the question is, is there an effective way that helps when creating complex user forms ?

7

I’ve sorted it out and in the end it went pretty well so I’ll give here my recommendations according this painful experience.

(these tips apply for WPF)

  • Validation : use INotifyDataErrorInfo instead of rules in XAML, your validation takes place in the object instead and even it’s sort of complicate to setup it is really worth it. This interface is appropriate for validation of a complex object. Here’s an excellent article showing it : http://anthymecaillard.wordpress.com/2012/03/26/wpf-4-5-validation-asynchrone/ I’d suggest you to not copy/paste it but write it manually. (I did and it took me less time than copy/paste and trying to grasp what it does later)

  • Updating your object whenever user inputs values : it’s clearly better to do nothing during controls events such as TextChanged but to stick to validation as mentioned above. Hook to Validation.Error events of your controls for managing the logic of your API (in my case when ‘name’ is set the user cannot search by ‘familiarity’ so I disabled appropriate controls).

  • That leds to a search button that gets enabled only when all values are correct and this is actually the place where you’d grab all params and your stuff, all in one place, not scattered anymore through events of controls.

  • Also, do not hesitate to use a middle-man object even though it won’t contain all the fields of the original object it still proved to be helpful as that’s where I implemented INotifyDataDataErrorInfo. There was absolutely no way that I modify the source object which is fine and part of a library anyway ! For multiple choices a user can do on a ListBox, I didn’t use it, I just populated them and that’s it. (they are all valid by nature, in my case)

Now a few screenshots and explanations.

Here’s the API, not about 2 or 3 fields but 21, when I saw it first I knew it would be a pain to implement but still I underestimated it …

Here the search button gets enabled only when validation is correct,

Using an extremely simple method, luckily in my case only these 3 cases were needed.

    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();
    }

Here’s the output of the query, well the only bothering stuff compared to previous version is that for testing I have to search every time to see the result of the built query for which I assigned Alt-S as a time-saver. Previously I was fetching the result on each value changed in each control, while it looked simple as these were 5 lines methods it soon went to a nightmare. With the new method, I have one hand setting the fields with mouse the other pressing Alt-S and obviously a much simpler class to understand/use/debug.

Last screen : as you can see there isn’t much in it, the 3 top methods each call one of the methods below, I think they are self-explanatory. The helper methods are used for setting/retrieving values from controls and to form the query.

Last two samples :

Populating data :

    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);
    }

Retrieving parameters :

    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;
        }

    }

In my case nullables were vital as they allowed me to discard values when building the query.

For building the query I cheated somehow using 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 is a middle-man object that holds the desired value and the description is fetched from a value [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;
        }
    }
}

A described object :

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

(PropertyEnumConverter is a simple object that will convert these values according their JsonProperty attribute.)

So as you can see, I kept it stupidly simple but it took me some time as at first I had a more complex approach, again the simplicity proven really helpful though it sounds stupid sometimes.

I haven’t talked about the XAML side, briefly there’s absolutely nothing in it beside controls and templates, only bindings have these two properties set for them to validate. (also, the object implements INotifyPropertyChanged)

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

I hope this answer will help someone, I’ve tried to be as concise as possible but if you have any question/comment, ask below.

2

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật