4

I am trying to implement a search function where the results could be populated to a Gridview. I am using LINQ to query my db in c#.

The problem I am trying to figure out is, if the user want to search through multiple columns/fields with more than one search term, then how do I do that? So instance if I search a table with these 3 fields and I had some data like: > firstName | lastname | players# >>1. Michael | Jordan | 12 >>2. Michael | Jordan | 24 >>3. Michael | Jordan | 45 >>4. DeAndre| Jordan | 6 >>5. Jerome| Jordan | 44 >>6. Jordan| Sparks| 88

Now if I search for Jordan I get everything:

If I search Michael Jordan than I should get row #s 1,2,3 back.

If I search Jordan Sparks 88 than I should row # 6 back.

So my problem is I don't know where in the table the search term may be so I must search all columns/fields. In my current code, I have something where I go through each column/field name and use the Contains() then || ("or"), but it only works for 1 search term.

Is there an elegant and easy way to search and filter through a whole linq table so that the list is minimized? From there I will add that result to a datasource and then bind it.

@Sphinxxx I am using a typical gridview. It looks like this:

    <asp:GridView ID="GridView" runat="server" AllowSorting="True" PagerStyle-Mode="NumericPages"
                                AutoGenerateColumns="false" Width="100%" CssClass="gridView" OnPageIndexChanging="GridView_PageIndexChanging"
                                AllowPaging="True"  DataKeyNames="idPlayersList" OnRowCommand="GridView_RowCommand"
                                OnRowEditing="GridView_RowEditing" OnRowCancelingEdit="GridView_CancelEditRow"
                                OnRowUpdating="GridView_UpdateRow" OnRowDataBound="GridView_RowDataBound">
                                <RowStyle CssClass="rowStyle"></RowStyle>
                                    <asp:BoundField DataField="FirstName" HeaderText="First Name" SortExpression="FirstName" />
                                    <asp:BoundField DataField="LastName" HeaderText="Last Name" SortExpression="LastName" />
<asp:BoundField DataField="PlayersNumber" HeaderText="Players Number" SortExpression="PlayersNumber" />
<asp:TemplateField HeaderText="Team" SortExpression="Team">
                                    <EditItemTemplate>
                                        <asp:DropDownList ID="ddlTeam" runat="server" CssClass="dropdown" AutoPostBack="True"
                                            AppendDataBoundItems="true" DataTextField="TeamName" DataValueField="idTeam">
                                        </asp:DropDownList>
                                    </EditItemTemplate>
                                    <ItemTemplate>
                                        <asp:Label ID="lblTeam" runat="server" Text='<%# Bind("TeamName") %>'></asp:Label>
                                    </ItemTemplate>
                                </asp:TemplateField>

My current search function looks something like this:

/**
* This method is for button search functionality 
* 
*/
protected void btnSearch_Click(object sender, EventArgs e)
{

    // Call to Entity Model Framework
    DBModel.DBEntities context = new DBModel.DBEntities();
    
    string[] searchTerms = txtSearch.Text.Trim().Split('&');

//Prepare to build a "players" query:
IQueryable<DBModel.playersList> playersListQuery = context.playersLists;

foreach (var term in searchTerms)
{
    //Refine our query, one search term at a time:
    playersListQuery = playersListQuery.Where(p => p.isDeleted == false && (p.FirstName.Contains(term.Trim()) ||
                                           p.LastName.Contains(term.Trim()) ||
                                           p.PlayersNumber.Contains(term.Trim()) ||
                                           p.Team.Name.Contains(term.Trim())));
}

//Now we have the complete query. Get the results from the database:
var filteredplayersList = playersListQuery.Select(s => new
                                           {

                                               idPlayersList = s.idPlayersList,
                                               FirstName = s.FirstName,
                                               LastName = s.LastName,
                                               PlayersNumber  = s.PlayersNumber,
                                               TeamName  = s.Team.Name
                                            }).ToList();


GridView.DataSource = filteredplayersList; //Connecting query to the datasource Gridview
GridView.DataBind();  //Binding Gridview
}
Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
user2533789
  • 1,192
  • 3
  • 9
  • 14
  • Possible duplicate of [Is it possible to use Full Text Search (FTS) with LINQ?](http://stackoverflow.com/questions/224475/is-it-possible-to-use-full-text-search-fts-with-linq) – Sruit A.Suk Jun 13 '16 at 16:45

2 Answers2

6

Luckily, creating multiple ANDs (one for each search term) is easy in Linq2SQL - just keep adding .Where(.. to your query:

string[] searchTerms = "Jordan Sparks 88".Split(' ');

using (var dc = new MyDataContext())
{
    //Using DataContext.Log is handy 
    //if we want to look at Linq2SQL's generated SQL:
    dc.Log = new System.IO.StringWriter();

    //Prepare to build a "players" query:
    IQueryable<Player> playersQuery = dc.Players;

    //Refine our query, one search term at a time:
    foreach (var term in searchTerms)
    {
        //Create (and use) a local variable of the search term
        //to avoid the "outer variable trap":
        //http://stackoverflow.com/questions/3416758
        //http://stackoverflow.com/questions/295593
        var currentTerm = term.Trim();
        playersQuery = playersQuery.Where(p => (p.isDeleted == false)
                                                &&
                                               (p.FirstName.Contains(currentTerm) ||
                                                p.LastName.Contains(currentTerm) ||
                                                p.PlayersNumber.Contains(currentTerm) ||
                                                p.Team.Name.Contains(currentTerm))
                                            );
    }

    //Now we have the complete query. Get the results from the database:
    var filteredPlayers = playersQuery.Select(p => new
                                                   {
                                                       p.idPlayer,
                                                       p.FirstName,
                                                       p.LastName,
                                                       p.PlayersNumber,
                                                       TeamName = p.Team.Name
                                                   })
                                      .ToArray();

    //See if the generated SQL looked like it was supposed to:
    var sql = dc.Log.ToString();
}
Sphinxxx
  • 12,484
  • 4
  • 54
  • 84
  • So if I understand this correctly, it will go query the table the size of the searchTerm array amount of times. In each iteration of the loop, will it generate a table in which for the next iteration, a smaller filtered table is produced which is the filteredPlayers variable. – user2533789 Jul 02 '13 at 16:49
  • No, that's the beauty of Linq2SQL: The `foreach` loop only *prepares* a query (an `IQueryable`). The database table is only actually queried when you ask for the elements in the `IQueryable`, which happens implicitly in the call to `.ToArray()` at the end. Run the code and look at the `sql` string - there's only one SQL query (with X number of AND statements). – Sphinxxx Jul 02 '13 at 19:49
  • What if I had a foreign key (fk) to a Teams table. Wouldn't I just to do something like: playersQuery = playersQuery.Where(p => p.firstName.Contains(term) || p.lastName.Contains(term) || p.playersNum.Contains(term) || **p.Teams.Name.Contains(term))**; – user2533789 Jul 04 '13 at 05:58
  • > If so, I'm getting an error similar to this: Sys.WebForms.PageRequestManagerServerErrorException: Sys.WebForms.PageRequestManagerServerErrorException: DataBinding: 'DBModel.Players' does not contain a property with the name 'TeamName'. I'm trying to bind to a gridview. **GridView.DataSource = filteredRangeList; GridView.DataBind();** – user2533789 Jul 04 '13 at 06:03
  • 1: Yes, the FK query should work the way you've written it. 2: I'm only guessing, but that error seems to come from the `GridView`, not the sql query. Is the grid set up with a column that looks for a "TeamName" property? If so, the `DBModel.Players` class needs to have a property called "TeamName". What kind of `GridView` is this? WinForms? – Sphinxxx Jul 04 '13 at 20:36
  • I have a simple gridview. I edited my question to include a sample of the gridvew code frontend. – user2533789 Jul 05 '13 at 20:46
  • Ok. First, you should check if the *database query* itself returns the correct players (`filteredPlayers` in my example). If it does, the problem must occur later on, when you add/bind players to your GridView. I'm afraid I don't have any experience with the Asp.NET GridView web control, so I wouldn't know why it starts looking for a "TeamName" property all of a sudden.. – Sphinxxx Jul 05 '13 at 21:04
  • Log is not available to me for some reason. Do I need to import using System.Diagnostics; ? Also is there a write the results (filteredplayers) to the console? – user2533789 Jul 05 '13 at 23:08
  • The `Log` in the example is just a property on an ordinary Linq2SQL `DataContext` ("dc" in the example) - no extra references or usings should be necessary. Which console do you want to write stuff to? On the server? Can you instead find a way to run your code locally and just set breakpoints in Visual Studio? – Sphinxxx Jul 05 '13 at 23:29
  • @Spinxxx I'm using VS 2010. From your example would it be possible to write to that console or even to a text file. Any way I can verify the results. – user2533789 Jul 06 '13 at 23:05
  • You can write stuff to the "Output" window in Visual Studio ("View -> Output" from the main menu) like this: `System.Diagnostics.Debug.WriteLine("Hello");`. If that won't help you, just google "asp.net debugging" or "asp.net tracing". – Sphinxxx Jul 06 '13 at 23:24
  • Thanks I will try that out. Also I added what my original search query was. Is there a way to use select with new properties from your code. I think that is why I am getting that error. What is being binded to the gridview doesn't contain the field names. So I think it needs to select all the fields names. – user2533789 Jul 07 '13 at 21:28
  • I tried to add select but I keep getting this syntax error: **Error Cannot initialize type 'DBModel.PlayersList' with a collection initializer because it does not implement 'System.Collections.IEnumerable'** – user2533789 Jul 07 '13 at 22:28
  • You can for example add a `.Select()` when getting the `filteredPlayers`. I have updated my answer. – Sphinxxx Jul 07 '13 at 22:48
  • ok .. I was still getting a an error so I did this: **Select(s => new { idPlayers = ids.idPlayers, Description = s.Description, FirstName = s.FirstName, LastName = s.LastName, TeamName = s.Teams.Name }).ToList();** The syntax now works with the gridview. So testing it, I am not quite getting results I expected. If I search DeAndre Jordan, I get them all. What I think is happening, is it adds the row with DeAndre, then any rows with Jordan. – user2533789 Jul 07 '13 at 23:33
  • I am not sure if this the best solution, but is there a way to remove rows that I don't need? – user2533789 Jul 08 '13 at 00:13
  • So what I expected to happen was query on the first search term. Then using the same query results from the 1st search term, go to the next search term and filter from there and so on for any other search term. What we got is very close. Just need rows that contain all search terms in them. I am not sure if an **&&** operator is needed some where. – user2533789 Jul 08 '13 at 07:54
  • Searching for "DeAndre Jordan" should only give you one record using this solution. Did you write the `foreach` loop and the `.Where()` exactly the way I wrote it? If not, post your updated method (the whole thing). It's really hard to *guess* what might be wrong without seeing any code.. – Sphinxxx Jul 08 '13 at 13:21
  • Does your code even compile? In the querying part you use **.Team.Name**, but in the select you use **.TeamName.Name**? – Sphinxxx Jul 09 '13 at 13:31
  • There was a bug in my answer. This is a case of the "outer variable trap" (read [this](http://stackoverflow.com/questions/3416758) and [this](http://stackoverflow.com/questions/295593)). Basically, every separate `.Where()` condition from the `foreach` loop hangs on to the same `term` variable, and when the query finally is run (when creating `filteredPlayers`), only `term`'s last value ("Jordan") is used. I have updated my `foreach` loop so that every `.Where()` condition gets its own local term variable. – Sphinxxx Jul 09 '13 at 14:08
0

If you just need to use the Contains condition for all the properties, what you can do is to encapsulated all the properties that you need into a single one, then you can apply the LINQ expression with fewer fields, it would look cleaner.

public class Player
{
    public string SearchField { get => string.Format("{0}{1}{2}{3}",FirstName,LastName,PlayersNumber,Team); }

    public string FirstName {get; set;}

    public string LastName {get; set;}

    public string PlayersNumber {get; set;}

    public string Team {get; set;}
}

Even better I would recommend you to set the SearchField in the constructor when you create your object instance which will improve performance avoiding to create a substring for every invocation.

public string SearchField { get; private set; }

public Player(string firstName, string lastName, playersNumber, string team)
{
    FirstName = firstName;
    LastName = lastName;
    PlayersNumber = playersNumber;
    Team = team;
    SearchField = string.Format("{0}{1}{2}{3}",firstName,lastName,playersNumber,team);
}

Then you just filter by SearchField property

 foreach (var term in searchTerms)
 {
     var currentTerm = term.Trim();
     playersQuery = playersQuery.Where(p => p.isDeleted == false && 
                                            p.SearchField.Contains(currentTerm));
 }

Now you have a LINQ expression with fewer fields which gets you the exact same result set.

One more thing I just saw that you mentioned you were working with LINQ to SQL query, you could apply the same approach by creating a Computed Column in your SQL table that concatenate the value from multiple columns, you can do this since SQL Server 2012.

Hopefully someone finds this solution helpful.

Regards!

luis_laurent
  • 784
  • 1
  • 12
  • 32