Multi-Table Search

MultiTableSearch is a portal's global search: it searches several tables at once and lists what it finds by name — as cards, a list or a grid — each row with its table, its name and the columns its table's view shows, and what the search matched highlighted. Every table is read with a grid's security, a page at a time, so a table is read further only when the rows shown need it.

React Blazor

Search the demo's customers

Type a name, an email address or a city: the demo's accounts, contacts and opportunities are searched together, and each name opens its record's page. Choose a layout, sort the rows by name or by when they were last changed, or group them by table from Settings, and switch tables on and off from Filter, which says how many of each table's rows were found. Signed in, change what each table shows and save views of your own.

React example Blazor example React TypeScript Razor

Search from anywhere

MultiTableSearchBox is a search box for a page's header. As you type it suggests the first rows that match, each with its table, and picking one opens its record's page; See all results, first in the list, opens the page with the MultiTableSearch — here, the search above. With nothing typed, it offers your recent searches and the records you opened. The arrow keys move through the suggestions, Enter picks one and Escape closes them.

React example Blazor example React TypeScript Razor
React Blazor

SearchPageUrl (searchPageUrl) is the page with the MultiTableSearch, given what to search for as ?q=. Tables limits the tables searched, MaximumResults how many rows are suggested (6 unless set), and RecordUrl, OnSelect and ShowRecordLinks open a row as a MultiTableSearch's do. In React, give it an onNavigate so its pages open through your router.

Ctrl+K (⌘K on a Mac) anywhere on the page, and / where nothing is being typed, put the cursor in the box; KeyboardShortcut (keyboardShortcut) turns that off, as a page with two boxes does for one. With Collapsible (collapsible), a narrow screen shows the box as a search button until it is opened, and it folds away again once left empty.

React Blazor

A search on every page

Register the tables to search, put a MultiTableSearch on a page of its own, and the box in the layout's header pointing at it. Each row then opens its record's page. The project templates come set up this way, for accounts and contacts.

React Blazor

A search reads only the tables registered for it, with ForSearch. A table registered for searches alone stays out of timelines, calendars and maps, and needs no date; registering a table again adds to the controls it is for. Notes and activities are registered for timelines and calendars by default: to search one alone, remove it with RemoveMultiTableSource before registering it again.

Without a view of its own, a table is read through its Quick Find view: its rows titled by the table's primary name, showing the view's columns, and searched in its find columns — as Dynamics' own search reads it.

Views

A view registered for search says, table by table, which rows are searched and how they show. Each table's FetchXML carries all of it.

Each row is named by its table's title — its primary name unless the registration sets Title. The query's first <order> is what the rows are listed in order of, and shows first among their columns where it isn't the title; its <attribute>s are the columns they show, a joined table's among them; its quick-find filter (isquickfindfields="1") names the columns a search looks in; and its other filters choose the rows. A table with no query is read through its Quick Find view, in order of its title.

The rows of several tables are listed in one order, so every table must be ordered by a column of the same type — text, a number or a date. A table ordered by a column of another type than the first table's is left out of the search and reported, and the rest are shown.

Sorting

OrderBy (orderBy) sorts the rows of every table the same way, whatever its view says: by its title, by when the row was last changed (ModifiedOn) or by when it was made (CreatedOn); View keeps each view's own order. Either way the rows keep their names, and a date they are sorted by shows first among their columns. The user chooses the order from Settings, and a saved view keeps it.

React Blazor

BestMatch, the default, puts the rows whose title matches the search first, by title — ascending, an exact title before the longer ones it begins — then every other row that matches, by title. The ranking holds across tables and pages: no other row shows while any table has a title match left to read. Set View to keep each view's own order instead.

React Blazor
React Blazor

Searching

A search works as a grid's does: a row matches when a search column starts with what was typed, or contains it after a leading *, ignoring case and accents. Lookups match by their name, choices by their label, and number and date columns by >, <, >= or <= before a number, a day or a year.

Each row shows which of its columns matched, highlighting what was typed; a search column that matched but isn't shown is listed under the others, so no row is found without a reason. With nothing typed, the search reads nothing.

Filter lists each table with how many of its rows were found, as do the headings of rows grouped by table: a table with more to read than it has shown says so — Accounts (25+) — and its count grows as more are read.

With ShowTotals (showTotals) each table is named with every row of it that matches — Accounts (1,254) — counted when the first page is read: one more query a table, run with the user's own access and split past Dataverse's limit of 50,000 rows an aggregate reads.

React Blazor

The arrow keys move from the search box into the rows and between them, and Home and End go to the first and the last; Up from the first goes back to the box.

A screen reader hears how many rows were found once a search has read them, and that there are more to load.

Filters

Filter offers three filters of every table at once: My records, Active records, and when the rows were last changed — in the last 7 or 30 days, or the last year. MineOnly, ActiveOnly and ModifiedWithinDays (mineOnly, activeOnly, modifiedWithinDays) set them as the search opens, and a saved view keeps them.

React Blazor

A user's records are those table security records them as owning: the contact columns its RowsOwnedByContact and OwnedBy rules set when a row is created, party lists included. My records is offered to signed-in users, and a table that records no owner for them isn't searched while it is on — the search names it. Active records are those whose status is 0, of the tables that have one.

Recent searches

The search keeps what the user searched for and the records they opened from it, and with nothing typed offers them again, with a way to clear them. They are kept in the user's browser, each user's apart, and shared with a MultiTableSearchBox. ShowRecent (showRecent) turns them off.

On a record's page

Regarding (regarding) searches only the rows about one record, through each table's regarding column: notes' objectid, activities' regardingobjectid (with the activities the record takes part in), and for other tables the column their registration names with Regarding.

React Blazor

A table with no such column has nothing about the record to search, so it is left out and named above the results.

Notes and attachments

Notes are registered for timelines already. Register them for search too, naming the columns a search looks in, as notes have no Quick Find view — their file name among them, and a search finds attachments by name:

A note's row is titled by its subject. Only the notes meant for the portal are read, as everywhere: those whose text starts with *WEB*. On a record's page, a search finds the notes about it.

Layouts

Layout chooses cards, a list or a grid, which the user can change from the toolbar unless AllowLayoutChange is off. GroupByTable shows each table's rows under its name with a Show more of their own, and Descending lists the rows from last to first. PageSize is how many rows show at a time, and users choose another from Settings among PageSizes (pageSizes): 10, 25, 50 and 100 unless set, an empty list offering no choice. A saved view keeps the one chosen.

React Blazor

ItemTemplate (renderItem in React) draws a row the page's own way.

Opening a row

A row's name links to its record's page, registered with AddRecordPage (see Record Pages), where its table has one. RecordUrl (recordUrl) makes it link elsewhere; OnSelect (onSelect) has the page handle the pick instead, for a row RecordUrl gives no link; and ShowRecordLinks (showRecordLinks) turns the record pages' links off.

React Blazor

In React, give the search an onNavigate, such as React Router's navigate, and a click on a link goes through your router; without one it is an ordinary link.

Saved views

With AllowSavedViews, signed-in users save the tables shown, what each shows and how the search is laid out as views of their own, offered beside the page's, and choose their default and hidden views. They are kept as views of several tables that name searches, so the views saved on timelines and calendars aren't offered here, nor these there. SavedViewsKey keeps a search's views to itself.

Filter's Edit filters opens each table's query as a tab: the column its rows are sorted by, the columns they show, the columns a search looks in and the rows it reads.

From Manage views, a user can give the columns of a view of theirs titles of their own, as a grid's columns can be retitled.

React Blazor

The search in the address

What is searched for is kept in the page's address as ?q=, so a link or a reload searches again, and a MultiTableSearchBox elsewhere on the site sends visitors to the page — the search follows the address when it changes under it. SearchParameterName names another parameter, or null keeps it out of the address.

Reading the pages yourself

IMultiTableQueryService.LoadPagesAsync (loadMultiTablePagesAsync in React) returns a page of each table in order, not merged, each with the paging cookie to read its next page from. MultiTablePageMerger merges them and says which tables to read for more.

React Blazor

MultiTableSearch Class

Parameters

Name
Type
Default
Description
ActiveOnlybool
False
Whether only active rows show at first: those whose status is 0, of the tables that have one.
AllowLayoutChangebool
True
Whether the user can choose the layout.
AllowSavedViewsbool
False
Lets signed-in users change what each table shows, save views of their own — the tables, what each shows and is searched by, and the layout — offered in the view picker beside the page's, and choose their default view and hide the views they don't use. Off by default. Needs the Power Portals Pro Core solution's multi-table view tables; an internal user also needs the Power Portals Pro Saved Views security role.
DefaultTablesIEnumerable<string>?
The tables searched at first; the user can switch on the others. Unset, every table.
DefaultViewIdGuid?
The view shown at first: one registered for search, by id. Unset, every table — or the user's default view.
Descendingbool
False
Whether the rows run from the last to the first at first.
GroupByTablebool
False
Whether the rows show by table at first, each table's under its name with more of its own to show.
ItemTemplateRenderFragment<MultiTablePageItem>?
A row drawn the page's own way, in place of the layout's.
LayoutMultiTableSearchLayout
Cards
How the rows are laid out at first.
MineOnlybool
False
Whether only the signed-in user's own rows show at first: those table security records them as owning (RowsOwnedByContact, OwnedBy). A table that records no owner for them isn't searched while it's on, and the search says so.
ModifiedWithinDaysint?
Only the rows changed in this many days show at first: 1 to 3,650. Unset, whenever they were changed.
OrderByMultiTablePageOrder
BestMatch
What the rows are ordered by at first: the best matches first unless set (MultiTablePageOrder.BestMatch). A column the rows are ordered by that isn't the title shows first among a row's columns.
PageSizeint
25
How many rows show at a time, and are read of a table at a time: 1 to 100.
PageSizesIReadOnlyList<int>
The numbers of rows at a time the Settings menu offers, each 1 to 100, with MultiTableSearch.PageSize among them. Default 10, 25, 50 and 100; none offers no choice.
RecordUrlFunc<MultiTablePageItem, string>?
Where a row's name links to, in place of its record's page registered with AddRecordPage; null for no link.
RegardingTableRecordReference?
Only the rows about this record, through each table's regarding column.
SavedViewsKeystring?
Names this search among the searches, so the views saved on it and the user's default and hidden views are its own. Without one, it shares its views with every search that has none. Letters, digits, -, _ and ..
Searchstring?
What to search for. With nothing, the search shows nothing until the user types.
SearchParameterNamestring?
q
The page address's query parameter the search is kept in, so a link or a reload searches again: q unless set. null keeps it out of the address.
ShowRecentbool
True
Whether the search keeps the user's searches and the records they open from it, and offers them while nothing is typed: in their browser, for each user apart, shared with a Components.MultiTableSearchBox. Default true.
ShowRecordLinksbool
True
Whether a row's name links to its record's page, registered with AddRecordPage, when neither MultiTableSearch.RecordUrl nor MultiTableSearch.OnSelect is given. Default true; a table with no page shows no link.
ShowTotalsbool
False
Whether each table's name shows how many of its rows match in all, counted with its first page — a query of each table more — rather than how many have been read so far.
SignInUrlstring
Account/Login
Where a user who isn't signed in is sent to sign in, from the saved views' menu, which asks them to first; they come back to this page. Defaults to Account/Login.
TablesIEnumerable<string>?
The tables the search may read, by logical name. Unset, every table registered for search.
Titlestring?
The heading shown where there is no view to choose.
Name: ActiveOnly
Type: bool
Default: False
Description: Whether only active rows show at first: those whose status is 0, of the tables that have one.
Name: AllowLayoutChange
Type: bool
Default: True
Description: Whether the user can choose the layout.
Name: AllowSavedViews
Type: bool
Default: False
Description: Lets signed-in users change what each table shows, save views of their own — the tables, what each shows and is searched by, and the layout — offered in the view picker beside the page's, and choose their default view and hide the views they don't use. Off by default. Needs the Power Portals Pro Core solution's multi-table view tables; an internal user also needs the Power Portals Pro Saved Views security role.
Name: DefaultTables
Type: IEnumerable<string>?
Description: The tables searched at first; the user can switch on the others. Unset, every table.
Name: DefaultViewId
Type: Guid?
Description: The view shown at first: one registered for search, by id. Unset, every table — or the user's default view.
Name: Descending
Type: bool
Default: False
Description: Whether the rows run from the last to the first at first.
Name: GroupByTable
Type: bool
Default: False
Description: Whether the rows show by table at first, each table's under its name with more of its own to show.
Name: ItemTemplate
Type: RenderFragment<MultiTablePageItem>?
Description: A row drawn the page's own way, in place of the layout's.
Name: Layout
Type: MultiTableSearchLayout
Default: Cards
Description: How the rows are laid out at first.
Name: MineOnly
Type: bool
Default: False
Description: Whether only the signed-in user's own rows show at first: those table security records them as owning (RowsOwnedByContact, OwnedBy). A table that records no owner for them isn't searched while it's on, and the search says so.
Name: ModifiedWithinDays
Type: int?
Description: Only the rows changed in this many days show at first: 1 to 3,650. Unset, whenever they were changed.
Name: OrderBy
Type: MultiTablePageOrder
Default: BestMatch
Description: What the rows are ordered by at first: the best matches first unless set (MultiTablePageOrder.BestMatch). A column the rows are ordered by that isn't the title shows first among a row's columns.
Name: PageSize
Type: int
Default: 25
Description: How many rows show at a time, and are read of a table at a time: 1 to 100.
Name: PageSizes
Type: IReadOnlyList<int>
Description: The numbers of rows at a time the Settings menu offers, each 1 to 100, with MultiTableSearch.PageSize among them. Default 10, 25, 50 and 100; none offers no choice.
Name: RecordUrl
Type: Func<MultiTablePageItem, string>?
Description: Where a row's name links to, in place of its record's page registered with AddRecordPage; null for no link.
Name: Regarding
Type: TableRecordReference?
Description: Only the rows about this record, through each table's regarding column.
Name: SavedViewsKey
Type: string?
Description: Names this search among the searches, so the views saved on it and the user's default and hidden views are its own. Without one, it shares its views with every search that has none. Letters, digits, -, _ and ..
Name: Search
Type: string?
Description: What to search for. With nothing, the search shows nothing until the user types.
Name: SearchParameterName
Type: string?
Default: q
Description: The page address's query parameter the search is kept in, so a link or a reload searches again: q unless set. null keeps it out of the address.
Name: ShowRecent
Type: bool
Default: True
Description: Whether the search keeps the user's searches and the records they open from it, and offers them while nothing is typed: in their browser, for each user apart, shared with a Components.MultiTableSearchBox. Default true.
Name: ShowRecordLinks
Type: bool
Default: True
Description: Whether a row's name links to its record's page, registered with AddRecordPage, when neither MultiTableSearch.RecordUrl nor MultiTableSearch.OnSelect is given. Default true; a table with no page shows no link.
Name: ShowTotals
Type: bool
Default: False
Description: Whether each table's name shows how many of its rows match in all, counted with its first page — a query of each table more — rather than how many have been read so far.
Name: SignInUrl
Type: string
Default: Account/Login
Description: Where a user who isn't signed in is sent to sign in, from the saved views' menu, which asks them to first; they come back to this page. Defaults to Account/Login.
Name: Tables
Type: IEnumerable<string>?
Description: The tables the search may read, by logical name. Unset, every table registered for search.
Name: Title
Type: string?
Description: The heading shown where there is no view to choose.

Events

Name
Type
Description
LayoutChangedEventCallback<MultiTableSearchLayout>
Raised when the user chooses another layout.
OnSelectEventCallback<MultiTablePageItem>
Raised when the user picks a row that has no link of MultiTableSearch.RecordUrl's. A page that handles the picks is linked to no record pages.
SearchChangedEventCallback<string>
Raised as the user changes what is searched for.
Name: LayoutChanged
Type: EventCallback<MultiTableSearchLayout>
Description: Raised when the user chooses another layout.
Name: OnSelect
Type: EventCallback<MultiTablePageItem>
Description: Raised when the user picks a row that has no link of MultiTableSearch.RecordUrl's. A page that handles the picks is linked to no record pages.
Name: SearchChanged
Type: EventCallback<string>
Description: Raised as the user changes what is searched for.
React Blazor

MultiTableSearchBox Class

Parameters

Name
Type
Default
Description
Collapsiblebool
False
Whether the box shows as a search button on a narrow screen (640 pixels or less) until the user opens it, as a header with little room has it. It folds away again once the user leaves it empty.
KeyboardShortcutbool
True
Whether Ctrl+K (⌘K on a Mac) anywhere on the page, and / where nothing is being typed, bring the box up and put the cursor in it. Default true; a page with two boxes leaves it on one.
MaximumResultsint
6
How many rows are suggested: 1 to 25, 6 unless set.
Placeholderstring?
The hint shown in the empty box, and its name to a screen reader. 'Search' unless set.
RecordUrlFunc<MultiTablePageItem, string>?
Where a row links to, in place of its record's page registered with AddRecordPage; null for no link.
SearchPageUrl*string
The page that shows a Components.MultiTableSearch, which 'See all results' and a recent search go to with what to search for in MultiTableSearchBox.SearchParameterName: relative to the app's base, as a link is.
SearchParameterNamestring
q
The search page's query parameter the search is given in: q unless set, as MultiTableSearch.SearchParameterName's.
ShowRecentbool
True
Whether the box offers the user's recent searches and the records they opened while nothing is typed, and keeps them as they search: in their browser, for each user apart. Default true.
ShowRecordLinksbool
True
Whether a row links to its record's page, registered with AddRecordPage, when neither MultiTableSearchBox.RecordUrl nor MultiTableSearchBox.OnSelect is given. Default true.
TablesIEnumerable<string>?
The tables searched, by logical name. Unset, every table registered for search.
Widthstring?
The box's width, as CSS: 100% unless set.
Name: Collapsible
Type: bool
Default: False
Description: Whether the box shows as a search button on a narrow screen (640 pixels or less) until the user opens it, as a header with little room has it. It folds away again once the user leaves it empty.
Name: KeyboardShortcut
Type: bool
Default: True
Description: Whether Ctrl+K (⌘K on a Mac) anywhere on the page, and / where nothing is being typed, bring the box up and put the cursor in it. Default true; a page with two boxes leaves it on one.
Name: MaximumResults
Type: int
Default: 6
Description: How many rows are suggested: 1 to 25, 6 unless set.
Name: Placeholder
Type: string?
Description: The hint shown in the empty box, and its name to a screen reader. 'Search' unless set.
Name: RecordUrl
Type: Func<MultiTablePageItem, string>?
Description: Where a row links to, in place of its record's page registered with AddRecordPage; null for no link.
Name: SearchPageUrl*
Type: string
Description: The page that shows a Components.MultiTableSearch, which 'See all results' and a recent search go to with what to search for in MultiTableSearchBox.SearchParameterName: relative to the app's base, as a link is.
Name: SearchParameterName
Type: string
Default: q
Description: The search page's query parameter the search is given in: q unless set, as MultiTableSearch.SearchParameterName's.
Name: ShowRecent
Type: bool
Default: True
Description: Whether the box offers the user's recent searches and the records they opened while nothing is typed, and keeps them as they search: in their browser, for each user apart. Default true.
Name: ShowRecordLinks
Type: bool
Default: True
Description: Whether a row links to its record's page, registered with AddRecordPage, when neither MultiTableSearchBox.RecordUrl nor MultiTableSearchBox.OnSelect is given. Default true.
Name: Tables
Type: IEnumerable<string>?
Description: The tables searched, by logical name. Unset, every table registered for search.
Name: Width
Type: string?
Description: The box's width, as CSS: 100% unless set.

Events

Name
Type
Description
OnSelectEventCallback<MultiTablePageItem>
Raised when the user picks a row that has no link of MultiTableSearchBox.RecordUrl's. A page that handles the picks is linked to no record pages.
Name: OnSelect
Type: EventCallback<MultiTablePageItem>
Description: Raised when the user picks a row that has no link of MultiTableSearchBox.RecordUrl's. A page that handles the picks is linked to no record pages.