Monday 22 May 2017

Storing the AppInsights key in web.config

When working with Application Insights your configuration has a unique key value called the instrumentation key, or more commonly the iKey, which is the unique value that AppInsights uses to associate any telemetry output from your code with the appropriate bucket of data within Azure. It’s just a GUID and is available in the Azure portal. typically this is defined inside the ApplicationInsights.Config file as follows…

<?xml version="1.0" encoding="utf-8"?>
<ApplicationInsights xmlns="http://schemas.microsoft.com/ApplicationInsights/2013/Settings">
  ...
  <InstrumentationKey>00000000-0000-0000-0000-000000000000</InstrumentationKey>
</ApplicationInsights>

This GUID needs to contain the value from within your azure subscription. So far so good. The problem (such as it is) is that for a whole host of reasons I’d rather define this key within the web.config rather than a separate configuration file, as this makes managing keys much easier. If I’m deploying straight to Azure, I’d like to define app settings in Azure so as to override this value for my live environment. Or, if I were using Octopus deploy, I’d like to be able to define a variable and have a different value per environment (Test/Staging/Live and so on). Using the ApplicationInsights.Config file for this makes life a lot more difficult than it needs to be.

Now, I’ve looked at loads of suggestions as to how to overcome this issue – but none (other than the one I’m about to present here) worked for me. Your mileage might vary however. What I wanted was a simple way to write some code in order to read the iKey from my web.config, such that I could roll out my website and have the appropriate key updated for me. In order to do this I had to pull out my trusty copy of .NET Reflector and look at the code. I found a number of posts in the web that suggesting updating the value of TelemetryConfiguration.Active.InstrumentationKey, but regardless of where I set this it didn’t work.

During my spelunking through the code I did find that it’s possible to override the value from the ApplicationInsights.Config file with an environment variable – that being APPINSIGHTS_INSTRUMENTATIONKEY. If this value is defined on the machine you release on then fine, you’re in business. I can see that this is a decent option if you own the hardware that this is running on as someone from IT can login, set the value, and be done with it. But who owns servers these days? I wanted something that was effectively “zero-touch”, and environment variables are not exactly that – nor did I want to write a .ps script that would set this, so I headed out to find another option.

Initialization

In my web project I’d used the defaults for App Insights, which updated my web.config to add the following HTTP module into the processing pipeline…

<system.web>
  <compilation debug="true" targetFramework="4.5.2" />
  <httpRuntime targetFramework="4.5.2" />
  <httpModules>
    <add name="ApplicationInsightsWebTracking"
type="Microsoft.ApplicationInsights.Web.ApplicationInsightsHttpModule, Microsoft.AI.Web" />
  </httpModules>
</system.web>

Looking at this module, it called TelemetryConfiguration.Active (a static method) that constructs the telemetry configuration and, by default, reads it from the ApplicationInsights.Config file. From there it then reads the .config data and constructs all of the “plug-ins”, initializes the lot, and that then starts emitting trace data. The initialization of the the plug-ins is done with this section of code from the TelemetryConfigurationFactory class…

private static void InitializeComponents(TelemetryConfiguration configuration, TelemetryModules modules)
{
    InitializeComponent(configuration.TelemetryChannel, configuration);
    InitializeComponents(configuration.TelemetryInitializers, configuration);
    InitializeComponents(configuration.TelemetryProcessorChain.TelemetryProcessors, configuration);
    if (modules != null)
    {
        InitializeComponents(modules.Modules, configuration);
    }
}

So, it configures the telemetry channel (where the telemetry goes), then the initializers and so on. I had a thought – what if I were to add a new class into this initialization phase which would read the iKey from web.config, then I’d be able to plug this into the pipeline and I’d be sorted.

The Code

I came up with this trivially simple class…

public class InstrumentationKeyTelemetryInitializer : ITelemetryModule
{
    public void Initialize(TelemetryConfiguration configuration)
    {
        var iKey = ConfigurationManager.AppSettings["ApplicationInsights:iKey"];

        if (!string.IsNullOrEmpty(iKey))
        {
            configuration.InstrumentationKey = iKey;
        }
    }
}

All it does is read from the .config file and, if the ApplicationInsights:iKey value has been defined, that’s used as the instrumentation key. I then updated my web.config to add on this new app setting…

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    ...
  </configSections>
  <appSettings>
    ...
    <add key="ApplicationInsights:iKey" value="4246A9AC-B3E8-473B-B75D-65DE6B2EEED6"/>
  </appSettings>
</configuration>

That’s not a real key by the way, I just made up a GUID. To plug this in I then altered the ApplicationInsights.Config file to add this new class to the TelemetryModules element…

<?xml version="1.0" encoding="utf-8"?>
<ApplicationInsights xmlns="http://schemas.microsoft.com/ApplicationInsights/2013/Settings">
  ...
  <TelemetryModules>
    <Add Type="MSADC.AppInsights.InstrumentationKeyTelemetryInitializer, MSADC.AppInsights"/>
    ... other modules here
  </TelemetryModules>
  ...
  <InstrumentationKey>00000000-0000-0000-0000-000000000000</InstrumentationKey>
</ApplicationInsights>

Note that I have specifically set the instrumentation key to zeros, as I can easily spot that in the debug output if it’s wrong. I then ran the app and everything worked fine. Well, it nearly did. My initializer was called, but after all the others – so there were a few telemetry calls that included the “zero” key which was of no use at all. What I realised was that my telemetry initializer had to be the first in the .config file. With that done, everything did indeed work just as expected. The first call I got from the telemetry logging was this…

Application Insights Telemetry:
{
  "name": "Microsoft.ApplicationInsights.Dev.4246a9acb3e8473bb75d65de6b2eeed6.Message",
  "time": "2017-05-22T14:53:56.3869277Z",
  "iKey": "4246a9ac-b3e8-473b-b75d-65de6b2eeed6",
  "tags": {
    "ai.operation.parentId": "Oezb1HoAPP8=",
    "ai.location.ip": "127.0.0.1",
    "ai.cloud.roleInstance": "DESKTOP-6NDCRK5",
    "ai.operation.name": "GET /",
    "ai.operation.syntheticSource": "SDKTelemetry",
    "ai.application.ver": "1.0.0.0",
    "ai.operation.id": "Oezb1HoAPP8=",
    "ai.internal.sdkVersion": "dotnet:2.2.0-54037"
  },
  "data": {
    "baseType": "MessageData",
    "baseData": {
      "ver": 2,
      "message": "AI: Diagnostic message: Performance counters are unavailable {snip!}",
      "properties": {
        "Tags": "API",
        "DeveloperMode": "true"
      }
    }
  }
}

Voila! I have my key used by AppInsights, and I can now setup replacement keys for staging/live in the appropriate tool (in my case Octopus).

Conclusion

This was a bit harder than I’d expected but I got a solution in the end. I can now use this initializer to allow me to define the AppInsights key in a more convenient spot.

I don’t have a code download for this – please feel free to cut & paste the code above into your project, and update the web.config file as required.

Wednesday 3 February 2016

Ignoring parameters in Swashbuckle

I’m working with a WebApi that is passed the HttpRequestMessage class which is always available in the ASP.NET WebApi pipeline. However, when the documentation is exposed in Swashbuckle, I get a textbox that shows up where it expects me to enter a value for this parameter.

The operation is defined as follows…

  public async Task Get(HttpRequestMessage request)

The problem is, when the documentation is emitted I end up with this ugly mess...

image

I set about finding a way to instruct Seashbuckle to ignore this parameter, and finding there isn’t anything in the box I set about writing a filter that would do this for me.

In Swashbuckle you can plug-in operation “filters” that can be used to alter the emitted data – the filter is passed the context of the operation being emitted, and you can monkey around with the data that pops out. All I had to do then was create a filter that would look for this datatype, and remove the corresponding data from the results. I ended up with this…

  public class IgnoreHttpRequestMessageOperationFilter : IOperationFilter
  {
    public void Apply(Operation operation, SchemaRegistry schemaRegistry, 
                      ApiDescription apiDescription)
    {
      apiDescription.ParameterDescriptions
        .Where(desc => desc.ParameterDescriptor.ParameterType 
            == typeof(HttpRequestMessage))
        .ToList()
        .ForEach(param =>
        {
          var toRemove = operation.parameters
            .SingleOrDefault(p => p.name == param.Name);

          if (null != toRemove)
            operation.parameters.Remove(toRemove);
        });
    }
  }

With that class in place, I just needed to plug this in to the swagger config file as follows...

  c.OperationFilter();

And Bob's you're uncle...

image

Wednesday 6 January 2016

Auth0 and 401 Unauthorized

Today was One of those days™ where things just didn’t work well, or rather at all. I’ve been working on an ASP.NET WebAPI that was working fine against Auth0, but needed to update it to use OWIN. It took a while to update everything and get the appropriate packages installed, and then I went through the code and ripped out the old and replaced with the new.

The API is called using a bearer token that is retrieved from Auth0 – example code below…

  var uri = ConfigurationManager.AppSettings["auth0:Domain"];
  var client = new HttpClient { BaseAddress = new Uri(uri) };
  var data = new LoginRequest
  {
    Username = model.Username,
    Password = model.Password,
    ClientId = ConfigurationManager.AppSettings["auth0:ClientId"],
    Connection = ConfigurationManager.AppSettings["auth0:Connection"],
    GrantType = "password",
    Scope = "openid"
  };

  var response = await client.PostAsJsonAsync<LoginRequest>("oauth/ro", data);

  if (response.IsSuccessStatusCode)
  {
    var result = await response.Content.ReadAsAsync<LoginResponse>();

    // Now use the response token...
  }

I’d written this code myself to call the Auth0 API as I couldn’t get the provided Auth0 code to work at all – the response I was getting back was always a null. The LoginRequest and LoginResponse objects simply mimicked the request and response data sent to and received from the Auth0 API that I was calling.

When I called this I received a token back – but then the problems started. Whenever I called my API, all I got back was a 401 Unauthorized response. It took me a long time to sort this, I even went to the bother of creating an entirely new solution with two web apps included, one to “login” to Auth0 and get a token, the other being the API I wished to call, so that I could raise a support incident with Auth0. I had a feeling that it must still be something I was doing wrong as I’ve used Auth0 for a while and to be fair it had previously been working fine until I did this upgrade.

It got to the point where I needed to get some help, and that came in the form of Reflector. I’m sure that Red-Gate should send me a free copy the number of times I’ve mentioned it in my blog. Not that I mind paying for it, it’s a fantastic tool. Anyhow, after some spelunking through the code I found that the Owin pipeline has a logging component, and also found out how to enable it. I added the following to my web.config (this was in the callee project)…

  <system.diagnostics>
    <switches>
      <add name="Microsoft.Owin" value="Verbose" />
    </switches>
  </system.diagnostics>

With that in place I then debugged my application and saw this in the debug output…

Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationMiddleware Error: 0 : Authentication failed
System.IdentityModel.Tokens.SecurityTokenException: Issuer not known.
   at Microsoft.Owin.Security.Jwt.JwtFormat.Unprotect(String protectedText)
   at Microsoft.Owin.Security.Infrastructure.AuthenticationTokenReceiveContext.DeserializeTicket(String protectedData)
   at Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationHandler.<AuthenticateCoreAsync>d__0.MoveNext()

Finally I had something to go on! I've worked a fair bit with JWT tokens over the last couple of years so happened to know that the issuer was one of the fields within the JWT, so then I knew that something was either not sending the right issuer, or when decoding the token on the API side the wrong issuer was being used. Now, just to be clear, I had made ZERO changes to my web.config between the old (non OWIN) version that worked, and this new version that didn't work. So I was suspicious to say the least.

So, I grabbed a copy of the token I was using and headed off to jwt.io which has a handy JWT decoder. You paste in the token and it will decode it for you, I saw this when I did mine…

HEADER:ALGORITHM & TOKEN TYPE

{
  "typ": "JWT",
  "alg": "HS256"
}

PAYLOAD:DATA

{
  "iss": "https://redacted.com/",
  "sub": "auth0|568bf53557c21c88287d9b03",
  "aud": "P7H7jdzZ4pSc9ifPU2XS7y03HXfcOYHG",
  "exp": 1452069269,
  "iat": 1452033269
}

The important part was that the issuer looked correct to me, so I then went looking at the API project to see what it was defined as there. In the web.config (which was simply copied from the Auth0 sample) I had this...

  <add key="auth0:ClientId" value="....." />
  <add key="auth0:ClientSecret" value="....." />
  <add key="auth0:Domain" value="redacted.com" />
  <add key="auth0:Connection" value="....." />

BINGO!

The auth0Domain value did not include the scheme or trailing slash, and this was being used by the Auth0 code as the issuer. With this knowledge I simply updated the web.config to https://redacted.com/ and sure enough my code started working again.

Hopefully this helps someone else, it took me quite a while to solve!

Thursday 28 May 2015

Windows Live Writer no longer works with Blogger

I'm not a frequent blogger, so have only been bitten by this issue today - but checking the internet for clues it seems I'm not the only one. I wrote a long blog post in Live Writer and then tried to upload it, only to see this error message come up...

Being a geek I looked into this further and found that it was an issue with how Windows Live Writer authenticates (i.e. logs in) to Blogger. Google have disabled the mechanism that WLW was using, and so at this present moment (28th May 2015) you can't use WLW with a Blogger account. Pants.

There is currently no workaround (for WLW that is) - you simply cannot use it.

If you're on the Blogger platform you can use their tools to blog with (that's what I'm using here), or try out some other blog tools on the web. For now I'll limp along with the Blogger UI, it's OK - but nowhere near as easy to use as Windows Live Writer. Sadly it seems that Microsoft are no longer maintaining WLW, so you may be waiting a long while for a fix.

Adventures in Application Insights - Part 1

If, like I was, you’re struggling to get anything out of Azure Application Insights, I hope to be of some help.

It all started well enough – I added App Insights to a web app, this setup the ApplicationInsights.config file for me and boom, I had some data in App Insights on the web. Then I wanted to write a custom event to the cloud so I added the following code into the app…

  var tc = new TelemetryClient();
  tc.TrackEvent("Testing");
  tc.Flush();

I ran my app and imagine my dismay when nothing happened. I checked around in blog posts, looked at the official documentation, tried a bunch of things – but stubbornly the above lines of code simply didn’t want to work for me.

Instrumenting a Console App

The web project I’m adding this to isn’t the simplest, so rather than hack it around too much I then decided to create a simple Console App and debug through it to see what was happening. Here it is in its entirety…

using Microsoft.ApplicationInsights;
namespace ConsoleApplication3
{
    class Program
    {
        static void Main(string[] args)
        {
            var tc = new TelemetryClient();
            tc.TrackEvent("Testing");
            tc.Flush();
        }
    }
}

Not a great deal going on there! Having seen the ApplicationInsights.config file in the other project I added one to my console app too – well, I copied it from the web app to be fair. I ran the app and once again – nothing. At that point I decided it was time to do some debugging – but I couldn’t find symbols for Microsoft.ApplicationInsights.dll (and others from the App Insights stable), so I dragged out my trusty copy of Reflector and created some. If you didn’t already know, Reflector can generate missing PDB’s for you, it’s cooler than a penguins chilly bits and well worth the cost.

With a .pdb in hand I then stepped into the TrackEvent method and further down the call chain until I got to this point in the code…

[EditorBrowsable(EditorBrowsableState.Never)]
public void Track(ITelemetry telemetry)
{
  if (this.IsEnabled()
  {
    string instrumentationKey = this.Context.InstrumentationKey;
    if (string.IsNullOrEmpty(instrumentationKey))
    {
      instrumentationKey = this.configuration.InstrumentationKey;
    }
    if (!string.IsNullOrEmpty(instrumentationKey))
  ...

When I inspected the instrumentationKey it was empty - and this indicated to me that despite having an ApplicationInsights.config file, this wasn't actually being picked up. A quick look on disk and I saw that the file wasn't in the same directory as my .exe, so I went back to the app and set the properties of the file as shown below...

The main thing to note is that I changed Copy To Output Directory. With that altered I ran the app again and this time Boom, it threw an exception…

Drilling down on the exception the root cause was this…

{"Type 'Microsoft.ApplicationInsights.Extensibility.RuntimeTelemetry.RemoteDependencyModule, Microsoft.ApplicationInsights.Extensibility.RuntimeTelemetry' could not be loaded."}

The Config File

This alerted me to the content of the .config file that I'd blatantly copied and pasted from the web project, and opening it up it was evident what was happening - I'd 'told' it to load some types that didn't exist in my simple console app - or rather, the file I copied across included a bunch of types that were not referenced...

<?xml version="1.0" encoding="utf-8"?>
<?XML:NAMESPACE PREFIX = "[default] http://schemas.microsoft.com/ApplicationInsights/2013/Settings" NS = "http://schemas.microsoft.com/ApplicationInsights/2013/Settings" /><applicationinsights xmlns="http://schemas.microsoft.com/ApplicationInsights/2013/Settings">
  <!-- 
    Learn more about Application Insights configuration with ApplicationInsights.config here: 
    http://go.microsoft.com/fwlink/?LinkID=513840
    
    Note: If not present, please add <InstrumentationKey>Your Key</InstrumentationKey> to the top of this file.
  -->
  <telemetrymodules>
    <add type="Microsoft.ApplicationInsights.Extensibility.Implementation.Tracing.DiagnosticsTelemetryModule, Microsoft.ApplicationInsights"></add>
    <add type="Microsoft.ApplicationInsights.Extensibility.RuntimeTelemetry.RemoteDependencyModule, Microsoft.ApplicationInsights.Extensibility.RuntimeTelemetry"></add>
    <add type="Microsoft.ApplicationInsights.Extensibility.PerfCollector.PerformanceCollectorModule, Microsoft.ApplicationInsights.Extensibility.PerfCollector">
...

Here the TelemetryModules section is quite clearly defining a bunch of plug-ins, the first exists (as it's in Microsoft.ApplicationInsights which I have referenced), the second and third do not and sure enough the second is the subject of the exception I received. Armed with this knowledge I did a hatchet job on the .config file and removed anything I didn't have referenced from Nuget or directly. I then ran the app again and to my dismay - still nothing happened. I was expecting to see an HTTP Post request flying over the wire to Azure, but instead there was nothing.

Flush, aha, he saved every one of us!

(Sorry for the abysmal pun, I couldn’t help myself). I did a bit more digging on the TelemetryClient and found the Flush() method which I have added into the code samples above, but didn't have in my code at the time. With this added I was delighted to see a message show up inside Visual Studio that alerted me to the fact that I had managed to send an event to Azure..

I also saw this pop up in Fiddler...

So far so good. I then logged into Azure and lo and behold, I got my event!

I could even drill down and see “more” data about the event…

OK, so “more” data is somewhat subjective, but it did tell me that my event came from the UK which is correct. I have no doubt that I could augment the data being collected from my application so that the amount of detail here would be better.

Looking at what was sent to the server, it’s a simple JSON request as follows (I’ve blanked out my App Insights key, which I’m surprised to find twice in the content. If I’d designed this, the app insights key would have been a custom header, but that’s just an aside)…

And there you have it – one event in Azure, an hour or more messing around with it to understand what it’s doing, and I’ve got a load closer to working out why my web app wasn’t sending any telemetry (or rather, the custom telemetry I was expecting to send).

I hope that helps someone else! Next time I’ll see what’s up with my web app.

Sunday 10 May 2015

Process Monitor saves the day

For some time now on my main development machine I’ve been having issues – Visual Studio has begun taking an extremely long time to load, and when running or debugging web apps these too have been taking too long to startup.

It’s one of those issues where you put off fixing it for a while as it’s not too bad, but then it gets to the point where it’s really hampering the dev/test/debug cycle, so this morning I set about working out what it could be. I disabled the extensions I have loaded and that made little difference, so then went to ProcessMonitor to see if I could work out what was happening.

In the case of Visual Studio, it would take around a minute to start a fresh copy. This was way too long. For websites, it was maybe 30 seconds or so – again, way too long. With process monitor running I created a new instance of Visual Studio, waited until it was done, and then paused event capture and went to have a look into the events.

It wasn’t long before I noticed a *lot* of files being written to the C:\logs directory – on my machine I keep this for one of two file types, actual log files from my code, and log files from the assembly log viewer, FusLogVw.exe. Smoking gun in hand I had a quick look at the size of the Logs directory and it was up to 8GB! Oops.

A small while later (after disabling FusLogVw.exe and deleting the files), I’m back to snappy performance with VS loading in about 4 seconds, and websites in a second or so.

Normal service resumed. Phew!

Wednesday 28 January 2015

Xamarin Forms contacts search

I’m in the middle of writing a Xamarin Forms app and today I needed to add in a contacts search page, and remembered that James Montemagno had created a plugin that exposes contacts in a platform neutral manner so I downloaded it and used it in my app.

I also wanted to add a search bar, and again there’s a control in XF for that, so I ended up with (somewhat simplified) the following XAML…

  
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<SearchBar x:Name="search" Placeholder="Search">
</SearchBar>
<ListView Grid.Row="1" ItemsSource="{Binding FilteredContacts}"
IsGroupingEnabled="true" GroupDisplayBinding="{Binding Key}"
GroupShortNameBinding="{Binding Key}">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Name}" TextColor="Black"
Detail="{Binding PhoneNumber}" DetailColor="Gray">
</TextCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>

I have removed some of the XAML as it’s not that important to this post (it’s available in the download). Now, with that in place (and a load of code in the view model which I’ll get to in a minute) I got a UI as follows…


iOS Simulator Screen Shot 28 Jan 2015 22.32.19


So far so good. Then I needed to hook up the search box, and as I’m using XAML (and if you’re not, you should give it a try as it’s way easier to create UI’s using it) I needed a way to bind to the search box to that I could respond to the TextChanged event.


Another excellent package that you’ll want to use it the Xamarin.Behaviors package by Corrado – massive thanks to him for putting this together, it’s excellent!


Behaviors to the rescue


By adding a behavior into the XAML, I can handle an event – so in this case I added the following to the SearchBar…

  <SearchBar x:Name="search" Placeholder="Search">
<b:Interaction.Behaviors>
<b:BehaviorCollection>
<b:EventToCommand EventName="TextChanged"
Command="{Binding SearchTextChanged}"
CommandParameter="{Binding Text, Source={x:Reference search}}"/>
</b:BehaviorCollection>
</b:Interaction.Behaviors>
</SearchBar>

This hooks the TextChanged event of the search bar, calls the SearchTextChangedCommand on my view model, and passes through the value of the Text property of the search bar. Yay!.


Or not.


The problem I found was that my command was being passed a string, but it was the text before the ne character was entered, so say I pressed ‘X’ in an empty search bar, my code would be called with an empty string. Pressing ‘A’ next, my command would get ‘X’, and pressing ‘M” next, my code would get ‘XA’. I was always getting the data just prior to the new character – so I guess that the TextChanged event should be more clearly termed as the TextChanging event.


Anyway, I needed to fix this – so looked at the actual event and it contains two properties, the current text and the new value. All I needed to do was get the new vale of the event arguments and I’d be away.


To the best of my knowledge there is no way to bind to the event arguments, you need to write some additional code (this was true in WPF and Silverlight, I’ve done exactly the same there too). So, I cranked out a new behavior that attaches to the SearchBar’s TextChanged event, and calls the command with the new value of the text property. My XAML is therefore this…

  <SearchBar x:Name="search" Placeholder="Search">
<b:Interaction.Behaviors>
<b:BehaviorCollection>
<bh:SearchBarTextChanged Command="{Binding SearchTextChanged}"/>
</b:BehaviorCollection>
</b:Interaction.Behaviors>
</SearchBar>

Here I'm using my SearchBarTextChanged behavior to hook to the SearchTextChanged command, and sure enough now when I type in the search bar I get the desired effect. Excellent!


Filtering in code


In the view model I use James’ contacts plugin to select all contacts that have a mobile phone, and that have at least a forename or surname (my real code blew up on a colleagues iPhone as he has a contact with just a company name). I tuck this list away as the source, and then create a filtered collection from the source and the filter statement.

CrossContacts.Current.PreferContactAggregation = false;
var hasPermission = CrossContacts.Current.RequestPermission().Result;

if (hasPermission)
{
// First off convert all contacts into ContactViewModels...
var vms = CrossContacts.Current.Contacts.Where(c => Matches(c))
.Select(c => new ContactViewModel(c));

// And then setup the contact list
var grouped = from contact in vms
orderby contact.Surname
group contact by contact.SortByCharacter into contactGroup
select new Grouping (contactGroup.Key, contactGroup);

foreach (var g in grouped)
{
_contacts.Add (g);
_filteredContacts.Add (g);
}
}

The above code uses some Linq to select all contacts and group them by first character of their surname. I created a simple Matches(contact) function that checks that the contact has a mobile phone number and also that they have one of forename or surname.


Then I have the code that is called to filter the collection when you type text into the search bar...

private void FilterContacts(string filter)
{
_filteredContacts.Clear ();

if (string.IsNullOrEmpty(filter))
{
foreach (var g in this.Contacts)
_filteredContacts.Add (g);
}
else
{
// Need to do some filtering
foreach (var g in this.Contacts)
{
var matches = g.Where (vm => vm.Name.Contains (filter));

if (matches.Any())
{
_filteredContacts.Add (new Grouping (g.Key, matches));
}
}
}
}

This is a bit ropey but does the trick. As the collection is an ObservableCollection, any changes are shown in the UI immediately.


Code


I’ve created a simple example project (the one shown above) that you can download. Hopefully this will help someone out. I’ve not tried this on Android or Windows Phone as yet, but as none of the code is in the platform specific library I can’t see any reason for it not to work on those platforms too.


Bye for now!