This article is Day #6 in a series called 31 Days of Windows 8. Each of the articles in this series will be published for both HTML5/JS and XAML/C#. You can find additional resources, downloads, and source code on our website.
Yesterday we introduced Contracts by exploring how to add Settings to our applications. Today we’re going to build upon that with Search, and tomorrow Share. Search and Share are two very interesting contracts because they bring life to your app even when it’s not actually running. In the context of search this means your application can potentially expose itself to the user in a new manner.
For the past 10 years, “search” has typically been synonymous with search engines. A few year ago, search actually became mainstream in Windows and if you were like me, you became very used to hitting the start button and just typing the name of your program rather than looking for it. Today in Windows 8 you can just type and those results are nicely displayed.
Better yet, Windows 8 is really extending how we think about search and bringing it right into our Windows Store Apps. Now it’s not really any magic, we just wire up to a few events and display the right things to our users but it changes the typical “entry point” into our applications. Now, rather than our user opening our app, finding your search box and searching, they can hit search and be taken directly to the thing they were looking for inside your app without any other steps.
Getting Set Up
As we have done every day since the beginning of this series, we are going to start with the Blank App template. Again, the reason we do this is because there is so much extra code to filter through just to find the thing you’re looking for.
Once you’ve gotten your new project together (and replaced the default image set…you did that right? Don’t you remember Day #1? You can still download that set of images here.) Now we’re going to add Search to our application. Thankfully, basic search is only a click away.
Right-click on your project, and choose Add > New Item… > Search Contract
It asks you for a page name to go with this. I’ve used SearchResults.xaml, but you can call your page whatever you’d like. When you click the “Add” button, you should be prompted to add some additional files to your project.
We will discuss each of these files briefly a little later, but for now, you should now see a project solution that looks similar to this one:
At this point, you should be able to run your project, choose the Search charm, and start typing away. Here’s what my app currently looks like when I search for “taco.”
As you can see, it works, but we’re not really providing any value because we don’t have anything to search. That comes next. Before we get to that, let’s talk about the philosophy behind search in Windows 8.
Search Philosophy
When you think about traditional Windows 8 applications, they’re generally on the smaller side. You won’t find a version of Outlook, for example, that manages so many types of data. Instead, there’s a Calendar app, a Mail app, and a People app.
Windows 8 apps are supposed to do one thing, and do that one thing really well. Hopefully I am describing the app you’re building right now. Windows 8 applications can also be searched even when they’re not currently running. This means that when a user is searching for something, your app’s name and icon should immediately bring certain types of data into their mind.
For example, the iHeartRadio app. If I’m searching that, I’m likely looking for a radio station.
For the Weather application, I’m likely searching for a city.
Netflix, I’m probably looking for the title of a movie or show.
Finally, the People app assumes I’m looking for a person’s name.
So, as you’re building your application, ask yourself this question:
“What would users expect to search for in my app?”
Another important discussion about search revolves around the concept of “Search” vs. “Find.” The Search charm should NOT be used for looking inside a document. Search should always be something that can be done to your app, even if it’s not currently running.
Here is Microsoft’s specific guidance on how to use the Search Charm.
Making Search Work
So as we saw in our opening paragraphs, adding the Search Contract to your project is only the first step. Next, we need to provide some valuable data for the user to search. If you’ll recall in my Day #4 article, I created a small set of elements from the Periodic Table to demonstrate the SemanticZoom control.
We can use that same data in this example. In case you missed it, here’s the Element class:
namespace Day6_SearchContract
{
class Element
{
public double AtomicWeight { get; set; }
public int AtomicNumber { get; set; }
public string Symbol { get; set; }
public string Name { get; set; }
public string Category { get; set; }
public string State { get; set; }
}
}
For simplicity, I am going to populate a List<Element> in our SearchResults.xaml.cs file. I’ll start by creating a new method, BuildElementList() and calling it from our constructor, and then creating a new Element object for the 36 elements we’ll be using.
List<Element> elements = new List<Element>();
public SearchResults()
{
this.InitializeComponent();
BuildElementList();
}
private void BuildElementList()
{
elements.Add(new Element { AtomicNumber = 1, AtomicWeight = 1.01, Category = "Alkali Metals", Name = "Hydrogen", Symbol = "H", State = "Gas" });
elements.Add(new Element { AtomicNumber = 2, AtomicWeight = 4.003, Category = "Noble Gases", Name = "Helium", Symbol = "He", State = "Gas" });
elements.Add(new Element { AtomicNumber = 3, AtomicWeight = 6.94, Category = "Alkali Metals", Name = "Lithium", Symbol = "Li", State = "Solid" });
elements.Add(new Element { AtomicNumber = 4, AtomicWeight = 9.01, Category = "Alkaline Earth Metals", Name = "Beryllium", Symbol = "Be", State = "Solid" });
elements.Add(new Element { AtomicNumber = 5, AtomicWeight = 10.81, Category = "Non Metals", Name = "Boron", Symbol = "B", State = "Solid" });
elements.Add(new Element { AtomicNumber = 6, AtomicWeight = 12.01, Category = "Non Metals", Name = "Carbon", Symbol = "C", State = "Solid" });
elements.Add(new Element { AtomicNumber = 7, AtomicWeight = 14.01, Category = "Non Metals", Name = "Nitrogen", Symbol = "N", State = "Gas" });
elements.Add(new Element { AtomicNumber = 8, AtomicWeight = 15.999, Category = "Non Metals", Name = "Oxygen", Symbol = "O", State = "Gas" });
elements.Add(new Element { AtomicNumber = 9, AtomicWeight = 18.998, Category = "Non Metals", Name = "Fluorine", Symbol = "F", State = "Gas" });
elements.Add(new Element { AtomicNumber = 10, AtomicWeight = 20.18, Category = "Noble Gases", Name = "Neon", Symbol = "Ne", State = "Gas" });
elements.Add(new Element { AtomicNumber = 11, AtomicWeight = 22.99, Category = "Alkali Metals", Name = "Sodium", Symbol = "Na", State = "Solid" });
elements.Add(new Element { AtomicNumber = 12, AtomicWeight = 24.31, Category = "Alkaline Earth Metals", Name = "Magnesium", Symbol = "Mg", State = "Solid" });
elements.Add(new Element { AtomicNumber = 13, AtomicWeight = 26.98, Category = "Other Metals", Name = "Aluminum", Symbol = "Al", State = "Solid" });
elements.Add(new Element { AtomicNumber = 14, AtomicWeight = 28.09, Category = "Non Metals", Name = "Silicon", Symbol = "Si", State = "Solid" });
elements.Add(new Element { AtomicNumber = 15, AtomicWeight = 30.97, Category = "Non Metals", Name = "Phosphorus", Symbol = "P", State = "Solid" });
elements.Add(new Element { AtomicNumber = 16, AtomicWeight = 32.06, Category = "Non Metals", Name = "Sulfur", Symbol = "S", State = "Solid" });
elements.Add(new Element { AtomicNumber = 17, AtomicWeight = 35.45, Category = "Non Metals", Name = "Chlorine", Symbol = "Cl", State = "Gas" });
elements.Add(new Element { AtomicNumber = 18, AtomicWeight = 39.95, Category = "Noble Gases", Name = "Argon", Symbol = "Ar", State = "Gas" });
elements.Add(new Element { AtomicNumber = 19, AtomicWeight = 39.10, Category = "Alkali Metals", Name = "Potassium", Symbol = "K", State = "Solid" });
elements.Add(new Element { AtomicNumber = 20, AtomicWeight = 40.08, Category = "Alkaline Earth Metals", Name = "Calcium", Symbol = "Ca", State = "Solid" });
elements.Add(new Element { AtomicNumber = 21, AtomicWeight = 44.96, Category = "Transitional Metals", Name = "Scandium", Symbol = "Sc", State = "Solid" });
elements.Add(new Element { AtomicNumber = 22, AtomicWeight = 47.90, Category = "Transitional Metals", Name = "Titanium", Symbol = "Ti", State = "Solid" });
elements.Add(new Element { AtomicNumber = 23, AtomicWeight = 50.94, Category = "Transitional Metals", Name = "Vanadium", Symbol = "V", State = "Solid" });
elements.Add(new Element { AtomicNumber = 24, AtomicWeight = 51.996, Category = "Transitional Metals", Name = "Chromium", Symbol = "Cr", State = "Solid" });
elements.Add(new Element { AtomicNumber = 25, AtomicWeight = 54.94, Category = "Transitional Metals", Name = "Manganese", Symbol = "Mn", State = "Solid" });
elements.Add(new Element { AtomicNumber = 26, AtomicWeight = 55.85, Category = "Transitional Metals", Name = "Iron", Symbol = "Fe", State = "Solid" });
elements.Add(new Element { AtomicNumber = 27, AtomicWeight = 58.93, Category = "Transitional Metals", Name = "Cobalt", Symbol = "Co", State = "Solid" });
elements.Add(new Element { AtomicNumber = 28, AtomicWeight = 58.70, Category = "Transitional Metals", Name = "Nickel", Symbol = "Ni", State = "Solid" });
elements.Add(new Element { AtomicNumber = 29, AtomicWeight = 63.55, Category = "Transitional Metals", Name = "Copper", Symbol = "Cu", State = "Solid" });
elements.Add(new Element { AtomicNumber = 30, AtomicWeight = 65.37, Category = "Transitional Metals", Name = "Zinc", Symbol = "Zn", State = "Solid" });
elements.Add(new Element { AtomicNumber = 31, AtomicWeight = 69.72, Category = "Other Metals", Name = "Gallium", Symbol = "Ga", State = "Solid" });
elements.Add(new Element { AtomicNumber = 32, AtomicWeight = 72.59, Category = "Other Metals", Name = "Germanium", Symbol = "Ge", State = "Solid" });
elements.Add(new Element { AtomicNumber = 33, AtomicWeight = 74.92, Category = "Non Metals", Name = "Arsenic", Symbol = "As", State = "Solid" });
elements.Add(new Element { AtomicNumber = 34, AtomicWeight = 78.96, Category = "Non Metals", Name = "Selenium", Symbol = "Se", State = "Solid" });
elements.Add(new Element { AtomicNumber = 35, AtomicWeight = 79.90, Category = "Non Metals", Name = "Bromine", Symbol = "Br", State = "Liquid" });
elements.Add(new Element { AtomicNumber = 36, AtomicWeight = 83.80, Category = "Noble Gases", Name = "Krypton", Symbol = "Kr", State = "Gas" });
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Obviously, you’re going to want to pull your data from another location, but this is an article about searching, not about data sources. 🙂
If you look in the default Filter_SelectionChanged() method that was created in SearchResults.xaml.cs, you should see some comments that look like this:
// TODO: Respond to the change in active filter by setting this.DefaultViewModel["Results"]
// to a collection of items with bindable Image, Title, Subtitle, and Description properties
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Right beneath those comments, we are going to add our logic to pull the appropriate records from our data source. Before we do that, however, there are a bunch of assumptions made for you in this method, and it’s important to understand what those assumptions are before you write anything else.
The first assumption is that your search results are going be a collection of objects that each have an Image, Title, Subtitle, and Description property. It’s unlikely that your data has those properties, and you don’t necessarily want to force your data to conform to what they’ve set up for you. The reason for this assumption is pretty simple.
Briefly crack open your SearchResults.xaml page. Look for the GridView control named “resultsGridView,” as well as the ListView control named “resultsListView.” We’ve already covered these types of controls on Day #4, but they each have an ItemTemplate already defined. This is where all the assumptions live. They are using the default DataTemplate named “StandardSmallIcon300x70ItemTemplate.” If you take a look at your Common/StandardStyles.xaml file, you’ll find it in there, and it’s trying to bind to those four properties that I mentioned earlier. We don’t want to use this template, but it’s a good start.
What I have done is copy that entire DataTemplate, and moved it into my SearchResults.xaml page, in the Page.Resources section. Here’s what I’ve modified that section to look like:
<Page.Resources>
<CollectionViewSource x:Name="resultsViewSource" Source="{Binding Results}"/>
<CollectionViewSource x:Name="filtersViewSource" Source="{Binding Filters}"/>
<common:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
<!-- TODO: Update the following string to be the name of your app -->
<x:String x:Key="AppName">Search Contract Example</x:String>
<DataTemplate x:Key="ModifiedSmallIcon300x70ItemTemplate">
<Grid Width="294" Margin="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Background="{StaticResource ListViewItemPlaceholderBackgroundThemeBrush}" Margin="0,0,0,10" Width="40" Height="40">
<TextBlock Text="{Binding Symbol}" FontSize="32" FontWeight="Bold" TextAlignment="Center" VerticalAlignment="Center" />
</Border>
<StackPanel Grid.Column="1" Margin="10,-10,0,0">
<TextBlock Text="{Binding Name}" Style="{StaticResource BodyTextStyle}" TextWrapping="NoWrap"/>
<TextBlock Text="{Binding Category}" Style="{StaticResource BodyTextStyle}" Foreground="{StaticResource ApplicationSecondaryForegroundThemeBrush}" TextWrapping="NoWrap"/>
<TextBlock Text="{Binding State}" Style="{StaticResource BodyTextStyle}" Foreground="{StaticResource ApplicationSecondaryForegroundThemeBrush}" TextWrapping="NoWrap"/>
</StackPanel>
</Grid>
</DataTemplate>
</Page.Resources>
You can see that it’s almost identical to the original, but I’ve renamed it to “ModifiedSmallIcon300x70ItemTemplate.” The other differences are that I changed the Image control to another TextBlock, and changed the Bindings to the properties that my objects possess, Symbol, Name, Category, and State. I’ve also changed the name of the ItemTemplates being used by the GridView and ListView to point to my modified version as well. Now we can start providing some results to our SearchResults page.
Remember those comments from earlier? The ones in the Filter_SelectionChanged() method? Let’s revisit those, and add some code.
IEnumerable<Element> searchResults = from el in elements
where el.Name.ToLower().Contains(searchString)
orderby el.Name ascending
select el;
this.DefaultViewModel["Results"] = searchResults;
// Ensure results are found
object results;
IEnumerable<Element> resultsCollection;
if (this.DefaultViewModel.TryGetValue("Results", out results) &&
(resultsCollection = results as IEnumerable<Element>) != null &&
resultsCollection.Count() != 0)
{
VisualStateManager.GoToState(this, "ResultsFound", true);
return;
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
I’ve added a simple LINQ statement to pull the appropriate data from my data source, and assigned the results to this.DefaultViewModel[“Results”]. This is what will populate our GridView control.
I’ve also modified some of the default code in the section below to explicitly call out that I’m using an IEnumerable collection of Element objects. If our search term actually has some results, this logic will be what shows those results.
At this point, if you run your project and do a search, you should get something that looks like this:
You can also close the app entirely, perform a search on your machine, and then choose your app. It works exactly the same way.
At this point, you’d think we’re done. You’re wrong. We’ve got much more to talk about yet. Specifically, how to prompt the user with actual search values from your app that match what they’ve already typed. Let’s take a look at that next.
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Defining Search Suggestions
This is actually a pretty simple process, especially compared to what we’ve already accomplished in this article. In our same SearchResults.xaml page, we need to start with a reference to the SearchPane (this also requires the addition of the namespace Windows.ApplicationModel.Search). We also instantiate the SearchPane object in our constructor.
SearchPane searchPane;
public SearchResults()
{
this.InitializeComponent();
BuildElementList();
searchPane = SearchPane.GetForCurrentView();
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Once you’ve gotten that set up, the next step is to create an event handler for the SuggestionsRequested event. Create a new OnNavigatedTo method, like the one below, and add the new event handler. (Also remove it by creating a OnNavigatingFrom method as well).
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
searchPane.SuggestionsRequested += searchPane_SuggestionsRequested;
}
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
base.OnNavigatingFrom(e);
searchPane.SuggestionsRequested -= searchPane_SuggestionsRequested;
}
void searchPane_SuggestionsRequested(SearchPane sender, SearchPaneSuggestionsRequestedEventArgs args)
{
args.Request.SearchSuggestionCollection.AppendQuerySuggestions((from el in elements
where el.Name.ToLower().StartsWith(args.QueryText.ToLower()) || el.Symbol.ToLower().StartsWith(args.QueryText.ToLower())
orderby el.Name ascending
select el.Name).Take(5));
}
You can see that I am simply providing the SearchSuggestionCollection with a set of (maximum) five string values as recommendations. This is the most it can take, so I’m limiting my results to five at most.
That’s it! If you search for an element in this application now, you should see (based on my query) that if your typed characters match either an element name or symbol, and you’ll get up to five recommendations.
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Forcing Search When Keystrokes Are Detected
There’s one last trick we should talk about, and that’s allowing the user to open the search box simply by typing. By adding one simple statement to our OnLaunched and OnSuspending methods in our App.xaml.cs file, we can enable this functionality throughout our entire application.
Add this line to your OnLaunched method:
SearchPane.GetForCurrentView().ShowOnKeyboardInput = true;
And this line to your OnSuspending method to disable it when the app is closed:
SearchPane.GetForCurrentView().ShowOnKeyboardInput = false;
And that’s it! Run your app, and just start typing. The Search Charm will automatically open and start capturing your keystrokes!
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Declarations
There’s one last thing that I want to cover in this article, and this is more for your knowledge than something that actually needs to be done. When you added the Search Contract to your project, there were several files added to your project, but there was also a declaration made in your appmanifest file. If you open that file, and navigate to the Declarations tab, you should see something that looks like this:
Without this declaration, none of the search functionality we’ve added will be allowed to work. You’ll get an error saying that access is denied.
Just remember that you need to declare that you’re using Search before your application will actually be allowed to use those APIs.
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Summary
Today, we took took a very quick look into adding Search into your application. The Search contract provides a new way for your app to expose your application to it’s users. Now, you just need to determine what exactly you want to expose. Getting yourself in front of your users ensures people are engage in your application. The more ways they use it, the better life is.
You can download the entire sample solution here here:
Tomorrow, we’re going to the final step in our contracts exploration, Sharing. See you then!
Leave a Reply