hierarchical view with autocomplete

9 posts, 0 answers
  1. Luc
    Luc avatar
    27 posts
    Member since:
    Jan 2013

    Posted 07 Nov 2019 Link to this post

    Hello!

     

    First of all I'm posting tihs in the DropDownTree section but I'm not convinced that the DropDownTree is necessarily the right component for what I want.

    Basically I need to display something exactly like the autocomplete component but it must have a hierarchy, so the items displayed in this "autocomplete" can be a parent item or a child item and each child is linked to a parent and should be somehow intended. This looks like what the DropDownTree offers but I'm having several problems working with it.

     

    In order to get my data, I need to call an external service which takes two parameters, the language and the search string (which should be whatever the user typed in the autocomplete box).

    This is how I setup my DropDown:

    01.@(Html.Kendo().DropDownTree()
    02.  .Name("search")
    03.  //.DataTextField("name")
    04.  .Placeholder("Search")
    05.  .Filter(FilterType.Contains)
    06.  .MinLength(2)
    07.  .DataSource(dataSource => dataSource
    08.    .Read(read => read
    09.      .Action("Search", "Blabla", new { culture = Localizer.CultureName })
    10.      .Data("onAdditionalData")
    11.    )
    12.  )
    13.)
    14.<input type="hidden" id="searchFilter" name="searchFilter" />

     

    In onAdditionalData, I setup the two parameters that my Search action from the Blabla controller requires:

    1.<script>
    2.    function onAdditionalData() {
    3.        return {
    4.            language: "@Localizer.CultureName",
    5.            searchString: $("#searchFilter").val()
    6.        };
    7.    }
    8.</script>

    Since I didn't find an easy way to get the value that the user typed, I'm binding the filtering event like this and setting the value in a hidden field, which is then used by onAdditionalData to pass the search string:

    01.function dropdowntree_filtering(e) {
    02.    //get filter descriptor
    03.    var filter = e.filter;
    04.    $("#searchFilter").val(filter.value);
    05.}
    06.
    07.$(function () {
    08.    $(document).ready(function() {
    09.        var dropdowntree = $("#search").data("kendoDropDownTree");
    10.        dropdowntree.bind("filtering", dropdowntree_filtering);
    11.    });
    12.});

     

    Then my controller calls the service with the 2 parameters and that's fine, I get my results in essentially this format:

    01.[
    02.  {
    03.    "id":851,
    04.    "name":"Some name 1",
    05.    "idParent":null,
    06.    "children":[
    07. 
    08.    ]
    09.  },
    10.  {
    11.    "id":402,
    12.    "name":"Some name 2",
    13.    "idParent":null,
    14.    "children":[
    15.      {
    16.        "id":403,
    17.        "name":"Some name 3",
    18.        "idParent":402,
    19.        "children":null
    20.      },
    21.      {
    22.        "id":404,
    23.        "name":"Some name 4",
    24.        "idParent":402,
    25.        "children":null
    26.      }
    27.    ]
    28.  },
    29.  {
    30.    "id":923,
    31.    "name":"Some name 5",
    32.    "idParent":null,
    33.    "children":[
    34.      {
    35.        "id":929,
    36.        "name":"Some name 6",
    37.        "idParent":923,
    38.        "children":null
    39.      }
    40.    ]
    41.  },
    42.  {
    43.    "id":728,
    44.    "name":"Some name 7",
    45.    "idParent":727,
    46.    "children":[
    47.      {
    48.        "id":734,
    49.        "name":"Some name 8",
    50.        "idParent":728,
    51.        "children":null
    52.      }
    53.    ]
    54.  },
    55.  {
    56.    "id":757,
    57.    "name":"Some name 9",
    58.    "idParent":727,
    59.    "children":[
    60.      {
    61.        "id":763,
    62.        "name":"Some name 10",
    63.        "idParent":757,
    64.        "children":null
    65.      }
    66.    ]
    67.  },
    68.  {
    69.    "id":405,
    70.    "name":"Some name 11",
    71.    "idParent":null,
    72.    "children":[
    73.      {
    74.        "id":406,
    75.        "name":"Some name 12",
    76.        "idParent":405,
    77.        "children":null
    78.      }
    79.    ]
    80.  }
    81.]

     

    Now my problem is that I don't know how to build a result set that the dropdowntree can display properly. I've tried to build a list of DropDownTreeItem like this:

    01.public async Task<JsonResult> Search(string language, string searchString)
    02.{
    03.  IReadOnlyCollection<MyDto> results = await _myService.Search(language, searchString);
    04.  IList<DropDownTreeItem> items = new List<DropDownTreeItem>();
    05.  foreach(var r in results)
    06.  {
    07.    DropDownTreeItem item = new DropDownTreeItem
    08.    {
    09.      Id = r.Id?.ToString(),
    10.      Text = r.Name,
    11.      HasChildren = r.Children.Count > 0
    12.    };
    13.    if(r.Children.Count > 0)
    14.    {
    15.      foreach(var c in r.Children)
    16.      {
    17.        item.Items.Add(new DropDownTreeItem
    18.        {
    19.          Id = c.Id?.ToString(),
    20.          Text = c.Name,
    21.          HasChildren = false
    22.        });
    23.      }
    24.    }
    25.    items.Add(item);
    26.  }
    27.  return Json(items);
    28.}

     

    But that doesn't really work, the autocomplete does not contain any items then, for some reason that also doesn't populate anything in the autocomplete (I get "no data found" even though there are items). From the examples I've seen, it seems that DropDownTreeItem needs to call the search method recursively to get its children and this doesn't work for me because my service already returns me all the relevant children based on my search string.

    I've tried using the ToTreeDataSourceResult method like this:

    1.public async Task<JsonResult> Search(string language, string searchString)
    2.{
    3.  IReadOnlyCollection<MyDto> results = await _myService.Search(language, searchString);
    4.  var tree = results.ToTreeDataSourceResult(new DataSourceRequest(), a => a.Id, a => a.IdParent);
    5.  return Json(tree.Data);
    6.}

     

    that also doesn't populate anything in the autocomplete. (I get "no data found" even though the tree.Data has data).

     

    I'm starting to think that this DropDownTree is not the right component and maybe I can achieve what I want with a simple AutoComplete and some styling ?

    Hopefully my problem is clear enough with those details.

    Any help would be greatly appreciated,

    Thanks!

     

     

  2. Luc
    Luc avatar
    27 posts
    Member since:
    Jan 2013

    Posted 07 Nov 2019 Link to this post

    I also tried something else based on something I found in another post on this forum:

    01.function dropdowntree_filtering(e) {
    02.        //get filter descriptor
    03.        var filter = e.filter;
    04.        var ddt = $("#search").getKendoDropDownTree();
    05.        var ddtVal = ddt.value();
    06. 
    07.        $.ajax({
    08.            type: "GET",
    09.            url: "/en/Blabla/Search",
    10.            data: { searchString: filter.value, language: 'en' },
    11.            dataType: "json",
    12.            success: function (result) {
    13.                ddt.dataSource.data(result);
    14.            }
    15.        });  
    16.    }
    17. 
    18.    $(function () {
    19.        $(document).ready(function() {
    20.            var dropdowntree = $("#search").data("kendoDropDownTree");
    21.            dropdowntree.bind("filtering", dropdowntree_filtering);
    22.        });
    23.    });

     

    For this test I've changed the controller like this:

    01.private string lastQuery = null;
    02.private IReadOnlyCollection<MyDto> lastResult = null;
    03. 
    04.private async Task<IReadOnlyCollection<MyDto>> GetData(string language, string searchString)
    05.{
    06.  if(!string.IsNullOrWhiteSpace(searchString) && searchString.Equals(lastQuery))
    07.  {
    08.    return lastResult ?? new MyDto[0];
    09.  }
    10.  lastResult = await _myService.Search(language, searchString);
    11.  lastQuery = searchString;
    12.  return lastResult;
    13.}
    14. 
    15.public async Task<JsonResult> Search(int? id, [CanBeNull] string language, [CanBeNull] string searchString)
    16.{
    17.  var r = await GetData(language, searchString);
    18.  var result = r.Where(x => id.HasValue ? x.IdParent == id : x.IdParent == null).Select(item => new
    19.  {
    20.      id = item.Id,
    21.      Name = item.Name,
    22.      hasChildren = item.Children.Count > 0
    23.  }).ToArray();
    24.  return Json(result);
    25.}

    I still get in my controller with the correct values, I thought this would work with the recursion but it doesn't, but still I get 4 results (without children), but they don't appear in the dropdowntree.

     

     

  3. Luc
    Luc avatar
    27 posts
    Member since:
    Jan 2013

    Posted 07 Nov 2019 Link to this post

    I've added this screenshot to illustrate what we're hoping to achieve. We don't need the highlighting but basically the results are based on the input and the output should be on 2 levels, in bold for the parent and the children underneath with some indentation
  4. Luc
    Luc avatar
    27 posts
    Member since:
    Jan 2013

    Posted 07 Nov 2019 Link to this post

    Based on the test from my 2nd post, if I change the javascript dropdowntree_filtering function to this:

    01.function dropdowntree_filtering(e) {
    02.    //get filter descriptor
    03.    var filter = e.filter;
    04.    var ddt = $("#search").getKendoDropDownTree();
    05.    var ddtVal = ddt.value();
    06.    $.ajax({
    07.        type: "GET",
    08.        url: "/en/Blabla/Search",
    09.        data: { searchString: filter.value, language: 'en' },
    10.        dataType: "json",
    11.        success: function (result) {
    12.            //ddt.dataSource.data(result);
    13.            var ds = new kendo.data.HierarchicalDataSource({
    14.                data: result
    15.            });
    16. 
    17.            ddt.setDataSource(ds);
    18.        }
    19.    });  
    20.}

     

    Then the dropdowntree contains results! But I still don't have the children...

  5. Luc
    Luc avatar
    27 posts
    Member since:
    Jan 2013

    Posted 08 Nov 2019 Link to this post

    I finally managed to get it to almost work, there's still one problem. Basically there was an issue in my linq query, just for reference here's the code that almost works here:

    index.cshtml

    01.@(Html.Kendo().DropDownTree()
    02.  .Name("search")
    03.  .DataValueField("Id")
    04.  .DataTextField("name")
    05.  .HtmlAttributes(new { style = "width:100%" })
    06.  .Filter(FilterType.Contains)
    07.  .LoadOnDemand(false)
    08.  .MinLength(2)
    09.  .DataSource(dataSource => dataSource
    10.      .Read(read => read
    11.          .Action("Search", "Blabla", new { culture = Localizer.CultureName })
    12.          .Data("onSearchAdditionalData")
    13.      )
    14.  )
    15.)
    16. 
    17.<input type="hidden" id="searchFilter" name="searchFilter" />
    18.<script>
    19.  function onSearchAdditionalData() {
    20.    return {
    21.      language: "@Localizer.CultureName",
    22.      searchString: $("#searchFilter").val()
    23.    };
    24.  }
    25.</script>
    26. 
    27. 
    28.<script>
    29.    function dropdowntree_filtering(e) {
    30.        //get filter descriptor
    31.        var filter = e.filter;
    32.        $("#searchFilter").val(filter.value);
    33. 
    34.        var ddt = $("#search").getKendoDropDownTree();
    35.        ddt.dataSource.read();
    36.    }
    37. 
    38.    $(function () {
    39.        $(document).ready(function() {
    40.            var dropdowntree = $("#search").data("kendoDropDownTree");
    41.            dropdowntree.bind("filtering", dropdowntree_filtering);
    42.        });
    43.    });
    44.</script>

     

    BlaBlaController.cs

    01.public class BlablaController : Controller
    02.{
    03.  private readonly IMyService _myService;
    04. 
    05.  private string lastQuery = null;
    06.  private IReadOnlyCollection<MyDto> lastResult = null;
    07. 
    08.  public BlablaController(IMyService myService)
    09.  {
    10.    _myService = myService;
    11.  }
    12. 
    13.  private async Task<IReadOnlyCollection<MyDto>> GetData(string language, string searchString)
    14.  {
    15.    if(!string.IsNullOrWhiteSpace(searchString) && searchString.Equals(lastQuery))
    16.    {
    17.      return lastResult ?? new MyDto[0];
    18.    }
    19.    lastResult = await _myService.Search(language, searchString);
    20.    lastQuery = searchString;
    21.    return lastResult;
    22.  }
    23. 
    24.  public async Task<IActionResult> Search(int? id, string language, string searchString)
    25.  {
    26.    IReadOnlyCollection<MyDto> baseSet = await GetData(language, searchString);
    27.    IReadOnlyCollection<MyDto> flattenedSet = baseSet.SelectMany(x => x.Children).Union(baseSet).ToArray();
    28. 
    29.    var result = flattenedSet.Where(x => id.HasValue ? x.IdParent == id : (x.IdParent == null || (x.Children != null && x.Children.Any()))).Select(item => new
    30.    {
    31.        id = item.Id,
    32.        Name = item.Name,
    33.        hasChildren = item.Children?.Any() ?? false
    34.    }).ToList();
    35.    return Json(result);
    36.  }
    37.}

     

    The problem now is that with some queries it seems that the Search method is not being called recursively for the children like it normally is. Here are some examples of a query that works:

    01.id=null, language="de", searchString="hol"
    02.  flattenedSet.Select(f => new { f.Id, f.Name}).ToArray():
    03.    [0]: {{ Id = 449, Name = Holzhandwerker/in (Drechslerei) EFZ }}
    04.    [1]: {{ Id = 450, Name = Holzhandwerker/in (Weissküferei) EFZ }}
    05.    [2]: {{ Id = 862, Name = Holzbearbeiter/in EBA }}
    06.    [3]: {{ Id = 820, Name = Holzbildhauer/in EFZ }}
    07.    [4]: {{ Id = 448, Name = Holzhandwerker/in EFZ }}
    08.    [5]: {{ Id = 447, Name = Säger/in Holzindustrie EFZ }}
    09.  result:
    10.    [0]: { id = 862, Name = "Holzbearbeiter/in EBA", hasChildren = false }
    11.    [1]: { id = 820, Name = "Holzbildhauer/in EFZ", hasChildren = false }
    12.    [2]: { id = 448, Name = "Holzhandwerker/in EFZ", hasChildren = true }
    13.    [3]: { id = 447, Name = "Säger/in Holzindustrie EFZ", hasChildren = false }
    14. 
    15.#this then yields another query:
    16. 
    17.id=448, language="de", searchString="hol"
    18.  flattenedSet.Select(f => new { f.Id, f.Name}).ToArray():
    19.    [0]: {{ Id = 449, Name = Holzhandwerker/in (Drechslerei) EFZ }}
    20.    [1]: {{ Id = 450, Name = Holzhandwerker/in (Weissküferei) EFZ }}
    21.    [2]: {{ Id = 862, Name = Holzbearbeiter/in EBA }}
    22.    [3]: {{ Id = 820, Name = Holzbildhauer/in EFZ }}
    23.    [4]: {{ Id = 448, Name = Holzhandwerker/in EFZ }}
    24.    [5]: {{ Id = 447, Name = Säger/in Holzindustrie EFZ }}
    25.  result:
    26.    [0]: { id = 449, Name = "Holzhandwerker/in (Drechslerei) EFZ", hasChildren = false }
    27.    [1]: { id = 450, Name = "Holzhandwerker/in (Weissküferei) EFZ", hasChildren = false }

    This is exactly what I expect. The Search method is called again with the id of each element that has children.

    Now here's another similar query:

    01.id=null, language="de", searchString="back"
    02.  flattenedSet.Select(f => new { f.Id, f.Name}).ToArray():   
    03.    [0]: {{ Id = 403, Name = Bäcker/in-Konditor/in-Confiseur/in (Bäckerei-Konditorei) EFZ }}
    04.    [1]: {{ Id = 404, Name = Bäcker/in-Konditor/in-Confiseur/in (Konditorei-Confiserie) EFZ }}
    05.    [2]: {{ Id = 929, Name = Detailhandelsassistent/in (Bäckerei, Konditorei, Confiserie) EBA }}
    06.    [3]: {{ Id = 734, Name = Detailhandelsfachmann/-frau Beratung (Bäckerei/Konditorei/Confiserie) EFZ }}
    07.    [4]: {{ Id = 763, Name = Detailhandelsfachmann/-frau Bewirtschaftung (Bäckerei/Konditorei/Confiserie) EFZ }}
    08.    [5]: {{ Id = 406, Name = Lebensmitteltechnologe/in (Backwaren) EFZ }}
    09.    [6]: {{ Id = 851, Name = Bäcker/in-Konditor/in-Confiseur/in EBA }}
    10.    [7]: {{ Id = 402, Name = Bäcker/in-Konditor/in-Confiseur/in EFZ }}
    11.    [8]: {{ Id = 923, Name = Detailhandelsassistent/in EBA }}
    12.    [9]: {{ Id = 728, Name = Detailhandelsfachmann/-frau EFZ, Schwerpunkt Beratung }}
    13.    [10]: {{ Id = 757, Name = Detailhandelsfachmann/-frau EFZ, Schwerpunkt Bewirtschaftung }}
    14.    [11]: {{ Id = 405, Name = Lebensmitteltechnologe/-login EFZ }}
    15.     
    16.  result:
    17.    [0]: { id = 851, Name = "Bäcker/in-Konditor/in-Confiseur/in EBA", hasChildren = false }
    18.    [1]: { id = 402, Name = "Bäcker/in-Konditor/in-Confiseur/in EFZ", hasChildren = true }
    19.    [2]: { id = 923, Name = "Detailhandelsassistent/in EBA", hasChildren = true }
    20.    [3]: { id = 728, Name = "Detailhandelsfachmann/-frau EFZ, Schwerpunkt Beratung", hasChildren = true }
    21.    [4]: { id = 757, Name = "Detailhandelsfachmann/-frau EFZ, Schwerpunkt Bewirtschaftung", hasChildren = true }
    22.    [5]: { id = 405, Name = "Lebensmitteltechnologe/-login EFZ", hasChildren = true }

    The result variable contains the correct data, we have 6 parent entries, only the first one doesn't have children. And this query does not result in any recursive calls, this is the end of it and there is no data in my dropdown. Any idea why?

     

     

     

  6. Luc
    Luc avatar
    27 posts
    Member since:
    Jan 2013

    Posted 08 Nov 2019 Link to this post

    found it, needed to add .ServerFiltering(true) like so:

    01.@(Html.Kendo().DropDownTree()
    02.  .Name("search")
    03.  .DataValueField("Id")
    04.  .DataTextField("name")
    05.  .HtmlAttributes(new { style = "width:100%" })
    06.  .Filter(FilterType.Contains)
    07.  .LoadOnDemand(false)
    08.  .MinLength(2)
    09.  .DataSource(dataSource => dataSource
    10.      .Read(read => read
    11.          .Action("Search", "Blabla", new { culture = Localizer.CultureName })
    12.          .Data("onAdditionalData")
    13.      )
    14.      .ServerFiltering(true)
    15.  ))

     

     

    a

  7. Ivan Danchev
    Admin
    Ivan Danchev avatar
    2184 posts

    Posted 08 Nov 2019 Link to this post

    Hello Luc,

    Thank you for the follow up. We are glad you managed to properly configure the DropDownTree to use remote data and server filtering.

    Regards,
    Ivan Danchev
    Progress Telerik

    Get quickly onboarded and successful with your Telerik and/or Kendo UI products with the Virtual Classroom free technical training, available to all active customers. Learn More.
  8. Luc
    Luc avatar
    27 posts
    Member since:
    Jan 2013

    Posted 12 Nov 2019 Link to this post

    Thanks for the reply.

     

    I do have one more question regarding this component. It does not seem that I can set all the messages in particular error messages for the DropDownTree component: I'm able to set the clear, nodata, singletag and deletetag from the Messages method. But if for some reason there's a problem in fetching the data, I get a "request failed" with a "retry" button in my DropDownTree and I cannot find any ways to set the text for them. I tried including telerik js/messages/kendo.message.XXX.js files based on the current language but that doesn't fix the issue. Any idea how this can be done? In order to reproduce this, I start the site in visual studio, everything is loaded properly, then I stop the app from visual studio, and then I type something in the filter box of the DropDownTree component.

     

    Thanks!

  9. Ivan Danchev
    Admin
    Ivan Danchev avatar
    2184 posts

    Posted 14 Nov 2019 Link to this post

    Hello Luc,

    These messages come from the TreeView embedded in the DropDownTree. You can change them through the API, by getting a reference to the DropDownTree, then accessing its embedded treeview and setting the messages with the setOptions method, as shown below:

    <script>
    	$(document).ready(function () {
    		var ddt = $("#search").data("kendoDropDownTree");
    		var treeview = ddt.treeview;
    
    		treeview.setOptions({
    			messages: {
    				retry: "custom retry",
    				requestFailed: "custom request failed",
    			}
    		})
    	})
    </script>

    Regards,
    Ivan Danchev
    Progress Telerik

    Get quickly onboarded and successful with your Telerik and/or Kendo UI products with the Virtual Classroom free technical training, available to all active customers. Learn More.
Back to Top