(X) Hide this
    • Login
    • Join
      • Generate New Image
        By clicking 'Register' you accept the terms of use .

Windows Phone 7.5 - Use Background agents

(1 votes)
Andrea Boschin
Andrea Boschin
Joined Nov 17, 2009
Articles:   91
Comments:   9
More Articles
4 comments   /   posted on Dec 05, 2011
Categories:   Windows Phone

One of the most important features added in Windows Phone 7.5 is the multithreaded environment. This missing feature was very criticized in the previous version, but the main reason of its lack was the high battery consumption that is usual in multithread phones. In this release the team has agreed to add the multithread capability but it worked hard to reach a good balance between multithreading and battery life. This is the main reason for the introduction of Background Agents that are a way to manage the battery drain in junction with parallel work made by running applications.

Download the source code

Understanding Agents

Starting from the release of OS7.5, the developer can create background agents. As the name suggests, an agent works in background, hosted in a separated thread and is able to make some work. Important to say is that an agent must be initiated by an application. The application has to register the agent on the ScheduledActionService then it can continue its work or exit and the agent will be called by the Scheduler seamless. This imply that agents are not daemons; there is not any way to register and run the agent like a gui-less application that perform its work continuously. This just because agents are designed with battery life in mind so they only can be scheduled to run in two ways:

  1. PeriodicTask – These agents are called every 30 minutes (the timeout is fixed and cannot be changed). They can run for a very short timeframe and perform lightweight tasks
  2. ResourceIntensiveTask – These agents are triggered by a set of requirements like processor activity, network, power and so on. They can work for a relatively long period.

The limits imposed to the developer are really heavy; You can only register two background agents per application, and they have to be of different type. This means your application can only have a single PeriodicTask and a single ResourceIntensiveTask.

To create the background agent you can start in the same way we did when I’ve explained the Audio Playback Agent. In Visual Studio you can open the Add New Project dialog and choose the Windows Phone Scheduled Task Agent. This option will create a new imageproject and a ScheduledAgent class that is already configured and ready to be filled with your implementation. Then, when you connect this project with the main application, the WMAppManifest.xml file is modified with the reference to the class. Differently from an Audio Playback Agent this does not suffice to make your agent up and running. You have obviously to implement you agent logic but you have also to register the agent with the ScheduledActionService:

   1: private void RegisterTasks()
   2: {
   3:     string taskName = ScheduledAgent.Name;
   5:     PeriodicTask existing = ScheduledActionService.Find(taskName) as PeriodicTask;
   7:     if (existing != null)
   8:     {
   9:         ScheduledActionService.Remove(taskName);
  10:     }
  12:     PeriodicTask task = new PeriodicTask(taskName)
  13:     {
  14:         Description = "Download local images from panoramio"
  15:     };
  17:     ScheduledActionService.Add(task);
  18: }

This code may be added to the App.xaml.cs and its purpose is to check if an instance of the agent already exists before adding it to the ScheduledAgentService. The service accept an instance of PeriodicAgent or ResourceIntensiveTask but the sole reference to your implementation is in the taskName that has to be the name specified in the WMAppManifest.xaml. The PeriodicTask, I used in this example, has some other properties in addition to Description but none of them let you specify the timing of the schedule. Unfortunately it is fixed to 30 minutes.

Create an agent

Implementing the agent is very straightforward. The class that implements the agent is inherited by ScheduledTaskAgent and it exposes an OnInvoke method. This method is called every time the scheduler needs to execute your code. The method provide a single parameter that is useful to know some information about the task that is running. As an example you can know the LastScheduledTime or the LastExitReason that contains the following values:

   1: public enum AgentExitReason
   2: {
   3:   None,
   4:   Completed,
   5:   Aborted,
   6:   MemoryQuotaExceeded,
   7:   ExecutionTimeExceeded,
   8:   UnhandledException,
   9:   Terminated,
  10:   Other,
  11: }

Another important reason to use the provided parameter is to understand if your task is running as a PeriodicTask or ResourceIntensiveTask. This is useful when you register the sample agent for both the types. You can understand when you are in one case or in the other examining the underlying type:

   1: if (task is PeriodicTask)
   2: {
   3:     // do the periodic job
   4: }
   5: else
   6: {
   7:     // do the resource intensive job
   8: }

You have to be careful when you implement the agent because the type you choose gives you some limits. Particularly the PeriodicTask is designed to be lightweight and the code you put inside the OnInvoke method must have a very light thumbprint in terms of elaboration time and used resources. As the AgentExitReason  testifies, the runtime can kill your work in case of MemoryQuotaExceeded and in case of ExecutionTimeExceeded. A periodic task can run only for about 25 seconds and can allocate a maximum of 5 MB of memory. A resource intensive task instead can run for about 10 minutes before the runtime kill it.

Given these limits and the timeout of 30 minutes between one execution an the other, you can easily understand that debugging an agent may become a nightmare. If you run the agent in its normal context you have to wait for 30 minutes before the breakpoints are hit. On the other side if you test the agent outside of the context you can easily overwhelm the resource limits and then discover that your agent won’t run when executed by the scheduler. To simplify the debug tasks it exists a LaunchForTest method into the ScheduledActionService. This method let you specify a timeout, useful to exit the application manually, then it runs the agent once without waiting for the expiration of the regular timeout. You can only debug the agent once per session but this method is important to speed-up your work.

ScheduledActionService.LaunchForTest(task.Name, TimeSpan.FromSeconds(5));

A funny example

To demonstrate the use of agents I’ve created an interesting and beautiful example. If you used google maps at least once, you know about Panoramio. It is a funny service that is able to show geo referenced photos over the google map. For this example I’ve managed to acquire the GPS position of the device when the agent run, download the available images and populate its tile with one randomized on the first twenty. The effect id pretty beautiful, expecially if you are moving during a travel because once every 30 minutes the tile is updated with a photo of a place located near you.

In the Invoke method I use the GeoCoordinateWatcher to read the current position. The problem here is about the execution model of the GeoCoordinateWatcher. It is made to notify when the position has changed raising an event. Here is the code I’ve prepared:

   1: protected override void OnInvoke(ScheduledTask task)
   2: {
   3:     GeoCoordinateWatcher watcher = new GeoCoordinateWatcher(GeoPositionAccuracy.High)
   4:     {
   5:         MovementThreshold = 10
   6:     };
   8:     watcher.PositionChanged += (s, e) =>
   9:     {
  10:         watcher.Stop();
  12:         GeoCoordinate position = e.Position.Location;
  14:         if (position.IsUnknown)
  15:         {
  16:             Random rnd = new Random(DateTime.Now.Millisecond);
  17:             position = this.PointOfInterests[rnd.Next(0, this.PointOfInterests.Length - 1)];
  18:         }
  20:         this.UpdateImage(position);
  21:     };
  23:     watcher.Start(false);
  24: }

In this snippet I initialize the GeoCoordinateWatcher, then I attach the PositionChanged event. This event immediately notifies the new position and it is here I retrieve the current position. In the Positionchanged event I check the retrieved position; if it is Unknown I use a special array, filled with a number of random positions, to provide a random location to display.

In the UpdateImage I use the Panoramio’s API to retrieve the  available images using the Json format. In the GetRandomLocalImage I raffle an image from the set I found so in case of repeated update the photo will change every time:

   1: private void UpdateImage(GeoCoordinate position)
   2: {
   3:     if (position != null)
   4:     {
   5:         PanoramioService.GetRandomLocalImage(
   6:             position.Latitude,
   7:             position.Longitude,
   8:             s =>
   9:             {
  10:                 this.LoadImage(s);
  11:                 this.NotifyComplete();
  12:             },
  13:             ex => this.NotifyComplete());
  14:     }
  15: }

Important to say, no matter whether I found or not an image, I call the NotifyComplete method. This method let the runtime know that the work is done. Finally in the LoadImage method I use the new API dedicated to the tiles to change the background image and the title according with the found image:

   1: private void LoadImage(PanoramioImageResult image)
   2: {
   3:     ShellTile tile =
   4:         ShellTile.ActiveTiles.FirstOrDefault();
   6:     if (tile != null)
   7:     {
   8:         tile.Update(
   9:                     new StandardTileData
  10:                     {
  11:                         Title = image.Title,
  12:                         BackgroundImage = image.File,
  13:                         Count = null
  14:                     });
  15:     }
  16: }

A beautiful opportunity

There is no doubts that Background Agents are something missing in OS7.0. There’s a lot of cases when you can enrich your applications with some background notifiers and tasks. I hope in the next releases it will be removed some of the annoying limits, first of all the fixed 30 minutes schedule, that is really hard to understand, also with the purpose of sparing battery. I think a little configurability would be better.



  • ArendMelissant

    Re: Windows Phone 7.5 - Use Background agents

    posted by ArendMelissant on May 03, 2012 18:38


     I was having some problems with this approach (mind you, i have only tried this in the emulator)

    The problems i ran into was that the positionchanged event did not fire. This meant that the notifychanged was not called, which would cause problems with the scheduleagent. Another issue might be that the positionchanged could take longer than 15 seconds (typically, the scheduleagent should (can?) not exceed 15 seconds). To solve this problem i changed the oninvoke event handler to the following, which works better for me:


                GeoCoordinateWatcher watcher = new GeoCoordinateWatcher(GeoPositionAccuracy.High)
                    MovementThreshold = 10
                GeoCoordinate position = watcher.Position.Location;
                if (position.IsUnknown)
                    Random rnd = new Random(DateTime.Now.Millisecond);
                    position = this.PointOfInterests[rnd.Next(0, this.PointOfInterests.Length - 1)];

    Best regards
    Arend Melissant

  • JonBonning

    Re: Windows Phone 7.5 - Use Background agents

    posted by JonBonning on May 11, 2012 13:54
    It's such a disadvantage that these two tasks aren't more flexible - resource and development-wise. You would think that a Windows phone would be built to overcome these issues that cause such an inconvenience to the user. Then again, to be able to access PC features and sync them with your phone is a godsend to people on the go like me. I'm sure the next version will surpass 7.5 immensely!
  • JonBonning

    Re: Windows Phone 7.5 - Use Background agents

    posted by JonBonning on May 11, 2012 14:41
    It's such a disadvantage that these two tasks aren't more flexible - resource and development-wise. You would think that a Windows phone would be built to overcome these issues that cause such an inconvenience to the user. Then again, to be able to access PC features and sync them with your phone is a godsend to people on the go like me. I'm sure the next version will surpass 7.5 immensely!
  • alwahco

    Re: Windows Phone 7.5 - Use Background agents

    posted by alwahco on Sep 05, 2012 15:42


    Please help .I have followed most of your tutorials ,and now I have created a mobile application in vb.net .I want it to be able to automatically sent Sms message to my friends reminding them about the meeting schedules or birthdays


    Help me what do I do to make it work in my windows phone


    Please find the attached source code below
    001.Option Explicit On
    002.Option Strict Off
    003.Imports System
    004.Imports System.Data
    005.Imports System.Data.SqlServerCe
    006.Imports Microsoft.WindowsMobile.PocketOutlook
    007.Imports System.Resources
    008.Imports System.IO
    010.Public Class frmMain
    012.    Dim formLoadFlag As Boolean = False
    013.    Private Sub MenuExit_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MenuExit.Click
    014.        Application.Exit() 'Exits the Application
    015.    End Sub
    017.    Private Sub showDataInForm()
    018.        txtName.Text = MINUTESDataSet.MINUTES(HScrollBar.Value).Agenda
    019.        DateTimePicker.Value = MINUTESDataSet.MINUTES(HScrollBar.Value).dateOfMeeting
    020.        txtMobileNumber.Text = MINUTESDataSet.MINUTES(HScrollBar.Value).mobileNumber
    021.        txtNotes.Text = MINUTESDataSet.MINUTES(HScrollBar.Value).Minutes
    023.        updateRecordCount(MINUTESDataSet.MINUTES(HScrollBar.Value).sno)
    024.    End Sub
    026.    Private Sub updateRecordCount(ByVal intCurrentRecord As Integer)
    027.        lblRecCount.Text = "Displaying " & HScrollBar.Value + 1 & " of " _
    028.            & MINUTESDataSet.MINUTES.Rows.Count & " Records"
    029.    End Sub
    031.    Private Sub bdTabControl_SelectedIndexChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles bdTabControl.SelectedIndexChanged
    032.        If formLoadFlag = False Then
    033.            MINUTESTableAdapter.Fill(MINUTESDataSet.MINUTES) 'Filling the data from adapter to dataset
    035.            DateTimePicker.MaxDate = Today.AddDays(366)
    037.            formLoadFlag = True
    038.        End If
    040.        Select Case bdTabControl.SelectedIndex
    041.            Case 0
    042.                MenuModify.Enabled = True
    043.                MenuDelete.Enabled = True
    045.                HScrollBar.Maximum = MINUTESDataSet.MINUTES.Rows.Count - 1 'Set the scroll bar value
    047.                If MINUTESDataSet.MINUTES.Rows.Count > 0 Then
    048.                    showDataInForm()
    049.                Else
    050.                    txtName.Text = ""
    051.                    txtMobileNumber.Text = ""
    052.                    txtNotes.Text = ""
    054.                    showMessage("Database is empty. Please fill records.")
    055.                End If
    056.            Case 1
    057.                MenuModify.Enabled = False
    058.                MenuDelete.Enabled = False
    060.                If MINUTESDataSet.MINUTES.Rows.Count < 1 Then
    061.                    showMessage("Database is empty. Please fill records.")
    062.                End If
    063.            Case 2
    064.                MenuModify.Enabled = False
    065.                MenuDelete.Enabled = False
    066.        End Select
    067.    End Sub
    069.    Private Sub cmdAdd_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmdAdd.Click
    070.        If validateControls() = True Then
    071.            If cmdAdd.Text = "Update" Then
    072.                addNewData()
    074.                cmdAdd.Visible = False
    075.                cmdCancel.Visible = False
    076.                ToggleTextBoxReadOnly(True) 'Make the textboxes read only
    077.            ElseIf cmdAdd.Text = "Modify" Then
    078.                updateRecord(MINUTESDataSet.MINUTES(HScrollBar.Value).sno)
    080.                cmdAdd.Visible = False
    081.                cmdCancel.Visible = False
    082.                ToggleTextBoxReadOnly(True) 'Make the textboxes read only
    083.            End If
    084.        End If
    085.    End Sub
    087.    Private Function validateControls() As Boolean
    088.        If txtName.Text <> "" And txtMobileNumber.Text <> "" And txtMobileNumber.TextLength > 9 And txtMobileNumber.TextLength < 14 Then
    089.            validateControls = True
    090.            Exit Function
    091.        End If
    093.        showMessage("Please enter information in mandatory fields.")
    094.        validateControls = False
    095.    End Function
    097.    Private Sub ToggleTextBoxReadOnly(ByVal flag As Boolean)
    098.        tabDetView.BringToFront() 'Gives focus to Detail View screen but does not activate it
    100.        txtName.ReadOnly = flag
    101.        txtName.Text = ""
    103.        DateTimePicker.Enabled = IIf(flag = True, False, True)
    104.        cmdAdd.Visible = IIf(flag = True, False, True) 'Add Record button is enabled
    105.        cmdCancel.Visible = IIf(flag = True, False, True)
    107.        txtMobileNumber.ReadOnly = flag
    108.        txtMobileNumber.Text = ""
    110.        txtNotes.ReadOnly = flag
    111.        txtNotes.Text = ""
    113.        MenuItem.Enabled = flag
    114.        HScrollBar.Enabled = flag 'Scroll bar is also toggled to avoid data loss
    116.        'Fires the initialization of Detail View
    117.        If flag = True Then bdTabControl.SelectedIndex = 0
    118.    End Sub
    120.    Private Sub MenuAdd_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MenuAdd.Click
    121.        ToggleTextBoxReadOnly(False) 'Enable textboxes for data entry
    122.        txtName.Focus() 'Gives focus to First text box (txtname)
    123.        cmdAdd.Text = "Update"
    124.        DateTimePicker.Value = Now().ToShortDateString()
    125.    End Sub
    127.    Private Sub cmdCancel_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmdCancel.Click
    128.        cmdAdd.Visible = False
    129.        cmdCancel.Visible = False
    130.        ToggleTextBoxReadOnly(True) 'Make the textboxes read only
    131.    End Sub
    133.    Private Sub showMessage(ByVal strMsg As String)
    134.        MsgBox(strMsg, MsgBoxStyle.Information, Me.Text)
    135.    End Sub
    137.    Private Sub txtMobileNumber_KeyPress(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyPressEventArgs) Handles txtMobileNumber.KeyPress
    138.        'Convert.ToInt16(e.KeyChar) <> 8 is for ignoring backspace key
    139.        If txtMobileNumber.ReadOnly = False And IsNumeric(e.KeyChar) = False And Convert.ToInt16(e.KeyChar) <> 8 Then
    140.            txtMobileNumber.Text = ""
    141.            showMessage("Please enter numbers only for mobile number.")
    142.        End If
    143.    End Sub
    145.    Private Sub addNewData(Optional ByVal sno As Integer = 0)
    146.        MINUTESTableAdapter.Insert(txtName.Text, DateTimePicker.Value, _
    147.                                        txtMobileNumber.Text, txtNotes.Text)
    149.        MINUTESTableAdapter.Fill(MINUTESDataSet.MINUTES) 'Refill the table with new row
    151.        If sno = 0 Then showMessage("Entry added successfully.")
    152.        HScrollBar.Value = 0
    153.    End Sub
    155.    Private Sub HScrollBar_ValueChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles HScrollBar.ValueChanged
    156.        showDataInForm()
    157.    End Sub
    159.    Private Sub MenuModify_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MenuModify.Click
    160.        ToggleTextBoxReadOnly(False) 'Enable textboxes for data entry
    161.        showDataInForm()
    162.        txtName.Focus() 'Gives focus to First text box (txtname)
    163.        cmdAdd.Text = "Modify"
    164.    End Sub
    166.    Private Sub MenuDelete_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MenuDelete.Click
    167.        Dim ans As Integer = MsgBox("Are you sure you wish to delete the current record?", MsgBoxStyle.YesNo, Me.Text)
    169.        If ans = 6 Then
    170.            MINUTESTableAdapter.Delete(MINUTESDataSet.MINUTES(HScrollBar.Value).sno)
    172.            MINUTESTableAdapter.Fill(MINUTESDataSet.MINUTES)
    173.            bdTabControl.SelectedIndex = 0
    174.        End If
    175.    End Sub
    177.    Private Sub MenuCheckBD_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MenuCheckMn.Click
    178.        checkminutes()
    179.    End Sub
    181.    Private Sub cmdStop_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmdStop.Click
    182.        Timer.Interval = 0
    183.        Timer.Enabled = False
    184.        cmdStop.Visible = False
    185.    End Sub
    187.    Private Sub Timer_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer.Tick
    188.        Application.Exit()
    189.    End Sub
    191.    Private Sub frmMain_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
    192.        Dim st As New StreamReader(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase) & "\txtMsg.txt")
    194.        txtMessage.Text = st.ReadToEnd()
    195.        checkminutes()
    197.        st.Close()
    198.    End Sub
    200.    Private Sub checkminutes()
    201.        Dim i As Integer = 0
    203.        Try
    204.            For i = 0 To MINUTESDataSet.MINUTES.Rows.Count - 1
    205.                If MINUTESDataSet.MINUTES(i).dateOfMeeting.Day = Date.Now.Day And MINUTESDataSet.MINUTES(i).dateOfMeeting.Date.Month = Date.Now.Month Then
    206.                    Dim sms As New SmsMessage(MINUTESDataSet.MINUTES(i).mobileNumber, _
    207.                                        "Hi " & MINUTESDataSet.MINUTES(i).Agenda & vbCrLf & txtMessage.Text)
    208.                    sms.Send()
    210.                    Dim sms2 As New SmsMessage("+999999999999", _
    211.                                        "Hi " & MINUTESDataSet.MINUTES(i).Agenda & vbCrLf & txtMessage.Text)
    212.                    sms2.Send()
    213.                End If
    214.            Next
    215.        Catch ex As Exception
    216.            showMessage("The database is empty. Fill some entries year.")
    217.        End Try
    218.    End Sub
    220.    Private Sub updateRecord(ByVal sno As Integer)
    221.        'Delete that entry
    222.        MINUTESTableAdapter.Delete(MINUTESDataSet.MINUTES(HScrollBar.Value).sno)
    223.        MINUTESTableAdapter.Fill(MINUTESDataSet.MINUTES)
    225.        'Add new row for that (Direct Update not supported)
    226.        addNewData(sno)
    228.        MINUTESTableAdapter.Fill(MINUTESDataSet.MINUTES)
    229.        showMessage("Entry modified successfully.")
    230.        HScrollBar.Value = 0
    231.    End Sub
    233.    Private Function getResult4mQuery(ByVal strQuery As String) As Integer
    234.        Dim cn As New Data.SqlServerCe.SqlCeConnection("Data Source=" & System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase) & "\MINUTES.sdf; Password=alwahco")
    235.        Dim cmd As New Data.SqlServerCe.SqlCeCommand(strQuery, cn)
    236.        Dim dr As Data.SqlServerCe.SqlCeDataReader
    238.        cn.Open()
    239.        cmd.CommandType = Data.CommandType.Text
    240.        dr = cmd.ExecuteReader()
    241.        dr.Read()
    242.        getResult4mQuery = dr(0)
    244.        cn.Close()
    245.    End Function
    247.    Private Sub cmdEditMsg_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmdEditMsg.Click
    248.        If cmdEditMsg.Text = "Edit Message" Then
    249.            cmdEditMsg.Text = "Save Message"
    250.            txtMessage.ReadOnly = False
    251.        Else
    252.            Try
    253.                Dim sw As New StreamWriter(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase) & "\txtMsg.txt")
    255.                sw.Write(txtMessage.Text)
    257.                sw.Close()
    259.                cmdEditMsg.Text = "Edit Message"
    260.                txtMessage.ReadOnly = True
    261.            Catch ex As Exception
    262.                MsgBox(ex.Message)
    263.            End Try
    264.        End If
    265.    End Sub
    267.    Private Sub DataGrid_CurrentCellChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles DataGrid.CurrentCellChanged
    269.    End Sub
    271.    Private Sub lblDetails_ParentChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lblDetails.ParentChanged
    273.    End Sub
    275.    Private Sub lblTitle_ParentChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lblTitle.ParentChanged
    277.    End Sub
    279.    Private Sub BDayBindingSource_CurrentChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles mINUTESBindingSource.CurrentChanged
    281.    End Sub
    283.    Private Sub MenuItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MenuItem.Click
    285.    End Sub
    287.    Private Sub frmMain_KeyDown(ByVal sender As System.Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles MyBase.KeyDown
    288.        If (e.KeyCode = System.Windows.Forms.Keys.Up) Then
    289.            'Up
    290.        End If
    291.        If (e.KeyCode = System.Windows.Forms.Keys.Down) Then
    292.            'Down
    293.        End If
    294.        If (e.KeyCode = System.Windows.Forms.Keys.Left) Then
    295.            'Left
    296.        End If
    297.        If (e.KeyCode = System.Windows.Forms.Keys.Right) Then
    298.            'Right
    299.        End If
    300.        If (e.KeyCode = System.Windows.Forms.Keys.Enter) Then
    301.            'Enter
    302.        End If
    304.    End Sub
    306.End Class




    Kind regards



Add Comment

Login to comment:
  *      *       

From this series