Entries:
Comments:
Posts:

Loading User Information from Channel 9

Something went wrong getting user information from Channel 9

Latest Achievement:

Loading User Information from MSDN

Something went wrong getting user information from MSDN

Visual Studio Achievements

Latest Achievement:

Loading Visual Studio Achievements

Something went wrong getting the Visual Studio Achievements

Changing time zones

In this article, I'll demonstrate how to use your current location to figure out and set the time zone.  It turns out that this isn't nearly as easy as I thought it would be, so read on to learn what went into this seemingly straight-forward idea.

This code requires Windows 7 due to the use of the location feature.  Since most systems don't include GPS chipsets (yet, anyway!), you'll also need software or hardware that can figure out the current location .  You can use the excellent (and free) GeoSense for Windows to make a best-guess location determination based on your current network affiliation, or buy a laptop with a GPS or cellular chipset and Windows 7 support.

SS-2010.03.09-23.51.23

The code for this project is available in both Visual Basic and Visual C# and will run in Visual Studio 2010 Express Editions.  If you don't have them, download them from here.

Understanding Time Zones

Time zones are funny things.  They're all about lining up time around the world so the sun rises and sets at roughly the same hour of the day.  I've always assumed that time zones were offset by an hour as you travel along a line of latitude.  This is how it works in the United States, but now I've learned that not everywhere in the world works this way.  I've also learned that not only is daylight savings optional, but the dates and amount of change can vary.  Finally, I've learned that it's not so easy to figure out your current time zone based on your location!

In Windows, you change your time zone by choosing from a long drop-down list.  In other words, it's completely in your hands to know that you've entered a time zone and to make the appropriate change.

Time_Zone_Settings-2010.04.15-10.58.47

Since Windows 7 is location-aware, we are starting to see applications that can fetch the local weather and news headlines, or perform local map search.  It seems only natural that time zone changes should be automated in the same manner.

Not Such a Trivial Task

When I started working on this project, I assumed that it would be trivial.  I even naively thought that I might be able to just make a system call to map coordinates to a time zone.  Not surprisingly, that didn't work.  It's a pretty big task to take the entire earth and map it to time zones, though clearly it must be possible!

After doing some searching, I discovered a web service that can do exactly what I was looking for - return the time zone when given a set of coordinates.  I assumed that I was done.  Silly me!

It turns out that the time zone name returned from the web service doesn't map to the Windows time zone name.  The web service uses a Unix format called Olson, used by the TZ utility.  Previously, I had thought that time zone names were standardized because I'm used to thinking of American time zones like Pacific Standard Time or Eastern Daylight Time.  It turns out that the correct name is neither Pacific Standard Time nor Pacific Daylight Time.  It's not even Pacific Time.  According to Windows, the name is Pacific Time (US & Canada).  The Olson name is America/Los_Angeles.  Fortunately, however, it's a one-to-one mapping.

Finding the Time Zone

Before the name even becomes a consideration, you need to subscribe to the system event for location change.  You can either use the LatLongLocationProvider or the CivicAddressLocationProvider in the Windows7.Location namespace (from the Windows API Code Pack), depending on whether you need latitude/longitude coordinates, or a civic address, respectively.  Create the provider, then subscribe to the LocationChanged event.  As with other Windows 7 device types, you need to have permission from the user to use it.  The RequestPermissions method checks for permission, and if not set, automatically prompts the user for permission.

 

Visual C#

gps = new LatLongLocationProvider(30000);

gps.LocationChanged += new LocationChangedEventHandler(gps_LocationChanged);

LocationProvider.RequestPermissions(IntPtr.Zero, true, gps);

Visual Basic

gps = New LatLongLocationProvider(30000)

LocationProvider.RequestPermissions(IntPtr.Zero, True, gps)

 

When the location changes, the LocationChanged event fires.  This provides a reference to the location provider and to the new location report.  Both the provider and the report will need to be cast to the specific type, either for LatLong or CivicAddress.  Once you have the address you need to determine the actual time zone.  The free/public source I found to perform the lookup was the timezone web service on GeoNames.org.  This service returns XML-formatted data including country, time zone name, and raw time offset values:

XML

<timezone>
    <countryCode>ES</countryCode>
    <countryName>Spain</countryName>
    <lat>39.5</lat>
    <lng>-5.97</lng>
    <timezoneId>Europe/Madrid</timezoneId>
    <dstOffset>2.0</dstOffset>
    <gmtOffset>1.0</gmtOffset>
    <rawOffset>1.0</rawOffset>
    <time>2009-11-02 01:31</time>
</timezone>

In this example, the time zone name is "Europe/Madrid."  The mapping from Olson to Windows is performed using a spreadsheet of mappings that I found on Tim Davis' blog.  This spreadsheet is a great resource.  In this example, the corresponding Windows time zone is "Central European Standard Time."  Since .NET uses the TimeZoneInfo object to represent time zones, we need to find the right object.  We do this by using the FindSystemTimeZoneById method of the TimeZoneInfo class.  Once you have the right TimeZoneInfo object (the Windows time zone entry), you need to prompt the user, and then update the system.

Setting the Time Zone

Unfortunately, setting the system time zone isn't as easy as retrieving it.  I assumed there would be a SetSystemTimeZoneById method, but once again, I was mistaken.  For some reason, you can easily enumerate zones or find the current one, but changing the current zone requires dropping into unmanaged code.  Considering that time zone is a per-user, non-administrative setting, I was surprised by this.

In the Win32 API's, in kernel32.dll, there are matching Set/GetDynamicTimeZoneInformation methods.  You can't use the managed date/time data types though, so I created additional logic to marshall data between the managed TimeZoneInfo and unmanaged DYNAMIC_TIME_ZONE_INFORMATION Win32 types.

NOTE: Pre Vista/Win7, the Win32 functions and types were named without the "Dynamic" keyword.  This was due to a less flexible layout for daylight savings.  Since this application requires Windows 7 for the location awareness, I decided to use the newer Win32 calls as well.

Visual C#

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct DynamicTimeZoneInformation
{
    [MarshalAs(UnmanagedType.I4)]
    public int bias;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
    public string standardName;
    public SystemTime standardDate;
    [MarshalAs(UnmanagedType.I4)]
    public int standardBias;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
    public string daylightName;
    public SystemTime daylightDate;
    [MarshalAs(UnmanagedType.I4)]
    public int daylightBias;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
    public string timeZoneKeyName;
    public bool dynamicDaylightTimeDisabled;
}

Visual Basic

<StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Unicode)> _
Public Structure DynamicTimeZoneInformation
  <MarshalAs(UnmanagedType.I4)> _
   Public bias As Integer
  <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=32)> _
   Public standardName As String
  Public standardDate As SystemTime
  <MarshalAs(UnmanagedType.I4)> _
    Public standardBias As Integer
  <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=32)> _
     Public daylightName As String
  Public daylightDate As SystemTime
  <MarshalAs(UnmanagedType.I4)> _
   Public daylightBias As Integer
  <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=128)> _
   Public timeZoneKeyName As String
  Public dynamicDaylightTimeDisabled As Boolean
End Structure

Create an extension method of the TimeZoneInfo class to convert to the DynamicTimeZoneInformation class, and wrap the Win32 SetDynamicTimeZoneInformation function.  Using an overload you can set the system time zone with either a TimeZoneInfo class or directly using DynamicTimeZoneInformation.

Setting the time zone is not quite as easy as just making the call though.  Users have the right to change time zone using the SE_TIME_ZONE_NAME privilege, but it's not enabled by default.  Use the system call, AdjustTokenPrivileges, to enable the privilege, make the time zone change, and then disable the privilege againBy itself, changing the time zone doesn't have any apparent effect except in new processes.  In order to see the change, you need to send a system notification message.  This notification is made by calling SendMessageTimeout with WM_SettingChange and a parameter of "intl".  So many things to do just to change the time zone!

Visual C#

const int WM_SETTINGCHANGE = 0x1a;
const int HWND_BROADCAST = (-1);
const int SMTO_ABORTIFHUNG = 0x2;

[DllImport("user32", EntryPoint = "SendMessageTimeoutA", CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = true)]
private static extern int SendMessageTimeout(int hwnd, int msg, int wParam, string lParam, int fuFlags, int uTimeout, ref int lpdwResult);

public static int BroadcastSettingsChange()
{
    int rtnValue = 0;
    return SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, "intl", SMTO_ABORTIFHUNG, 5000, ref rtnValue);
}

Visual Basic

Const WM_SETTINGCHANGE As Integer = &H1A
Const HWND_BROADCAST As Integer = (-1)
Const SMTO_ABORTIFHUNG As Integer = &H2

<DllImport("user32", EntryPoint:="SendMessageTimeoutA", CharSet:=CharSet.Ansi, SetLastError:=True, ExactSpelling:=True)> _
Private Function SendMessageTimeout(ByVal hwnd As Integer, ByVal msg As Integer, ByVal wParam As Integer, _
ByVal lParam As String, ByVal fuFlags As Integer, ByVal uTimeout As Integer, _ ByRef lpdwResult As Integer) As Integer End Function Public Function BroadcastSettingsChange() As Integer Dim rtnValue As Integer = 0 Return SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, "intl", SMTO_ABORTIFHUNG, 5000, rtnValue) End Function

Debugging and Installing

The Time Zone Changer is created as an addin for my MEF Utility Runner project.  Recall that this project was designed to run lots of small utilities to prevent loads of icons from showing in the notification area of the task bar (by the system clock).  The Visual Studio project needs a custom post-build event added in order to create the appropriate file structure:

SS-2010.05.03-20.33.30 The full command line commands should read as follows:

mkdir "$(TargetDir)Addins\MefUtil-TZChanger.util"
copy /Y "$(TargetDir)$(TargetFileName)" "$(TargetDir)Addins\MefUtil-TZChanger.util"
copy /Y "$(TargetDir)*.dll" "$(TargetDir)Addins\MefUtil-TZChanger.util"

 

In order to enable F5 debugging, set the project's Debug Start Action to Start external program to the path to the HostedWpfApp.exe executable:

SS-2010.05.03-20.36.01

When the project starts up, you'll see a notification to indicate the all was successful:

SS-2010.03.09-23.53.37

For final deployment, compress the contents of the .util folder (just the files within the folder) to a Zip file and change the extension from .zip to .util.  That's it!

Next Steps

Though I'm pretty proud of this project, two things could still really use some enhancements.  First, every location change will trigger a lookup to see if the computer is in a new time zone.  If it's the same time zone, you won't really see anything.  This leads to a lot of hits to the web service though. which in turn leads to the second enhancement.

If the system is offline (maybe you opened the laptop upon landing?), it may be able to get a location from a GPS chipset, but not be able to hit the location web service.  Currently this will result in an error (not fatal), but it won't try again when the system is back online.  This really should be fixed.

Conclusion

In the end, it seems like a pretty easy project: subscribe to location changes, lookup the time zone, and update the system.  What lessons I learned bringing it all together!  It is, however, a good example of how to leverage location in a new application.  Being able to know where you are can lead to some need apps.  I'm excited to see what comes of it.

About Arian

Arian Kulp is a software developer living in Western Oregon.  He creates samples, screencasts, demos, labs, and articles; speaks at programming events about data, UI, Silverlight, and more; and enjoys spending time with his family.

Tags:

Follow the Discussion

  • RobertRobert

    Great article. I've been looking for something like this.

  • Clint RutkasClint I'm a "developer"

    @ish did you install utilrunner.codeplex.com  This leverages our utilrunner app.

  • ishish

    has anyone been able to get this working

  • TonyTony

    Hi. I am an absolute beginner to C# but really need to get this to work for realistic reasons.

    When I open this project/solution in Vis C# Express 2010 and build it, it complains with the Windows API Code Pack dependency...so I downloaded and extracted that. But how do I reference it in this solution. I see a .sln file there as well but then it opens in its own solution... HELP please!

    Thanks,

    Tony.

  • IshIsh

    Hi There

    I did some more research and managed to get this working will put something together for the noobs like me once I finally get it working.

    I enjoyed the article and being some one who travels a lot in Europe appreciate the program you wrote.  I managed to follow your instructions and compile the MefUtil-TZChanger.util and load addon into MEF v2.  But I have run into a problem.   I get the error from MEF v2 when app is reloaded with addon.

    TaskDialog feature needs to load version 6 of comctl32.dll, but a different version is currently loaded in memory.

    I tried to add an app.manifest file to the project properties to set comctl32.dll to version 6 but  MEF  v2 doesn’t seem to have an  entry point to allow for mainfests.

    How do you suggest I solve this. Would really like to use this as there is nothing I have found to do the same job.  Im running windows 64 bit and the version of comctl32.dll is version 5 in the system 32 and syswow64 folder.

    1) Do I need to install version 6 of the Dll

    2) Or how do I force windows 7 to use the verison 6 dll

    Ish

Remove this comment

Remove this thread

close

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.