Build Better UIs with the Asynchronous Support in .NET 2.0

Build Better UIs with the Asynchronous Support in .NET 2.0

synchronous programming has always been a difficult and time-consuming process in Windows programming. Until now, developers typically wrote asynchronous code in .NET by using an asynchronous pattern with a Begin/End method that returned an IAsyncResult object, using either delegates or an explicit threading technique as the model. However, it’s far better to have an infrastructure-based approach with intrinsic support for asynchronous processing within .NET controls and libraries. The .NET framework version 2.0 takes this approach by providing a standard asynchronous pattern with a base infrastructure that helps you either use the intrinsic framework-supplied BackgroundWorker component in your classes or for building your components manually with asynchronous support. In addition, Whidbey natively supports asynchronous processing for many controls and for T-SQL statements. In this article, you’ll see how to use the model to fetch data asynchronously using a SqlCommand object, and how to build a class that uses the BackGroundWorker component.

Author Note: The sample code that accompanies this article uses VS.NET Beta for the samples. The DataBindingEx.sln requires the “Yukon” version of SQL Server, but the other samples do not.

Although this article focuses on asynchronous programming in Whidbey it is important that you understand the asynchronous model itself. The sidebar “How the Asynchronous Model Works” provides several examples. If you’re not thoroughly familiar with programming asynchronous operations, review the sidebar before reading the rest of this article.

Asynchronous Scenarios in .NET 2.0
The rest of this article contains a practical sample of programming asynchronous processes in .NET 2.0. The first scenario involves an asynchronous data fetch using the new Asynchronous support offered in the SqlCommand object. Later, I’ll show you how to use the new BackGroundWorker component.

Fetching Data Asynchronously

Author’s Note: The code in this section was drawn from the DataBindingEx.sln solution and the SQL Server scripts in downloadable sample code.

Asynchronous processing for fetching data is one of the big gains in Whidbey, especially in Windows Forms applications. You’ll see how easily this new functionality lets you add asynchronous processing to enhance the responsiveness of your applications.

The SqlCommand object provides several asynchronous call options. Each returns an IAsyncResult object. Table 1 shows their definitions:

Table 1. The SqlCommand object’s familiar methods now provide several asynchronous processing options as overloads.





Function BeginExecuteReader( _        ByVal callback As AsyncCallback, _        ByVal stateObject As Object, _        ByVal behavior As CommandBehavior) _             As IAsyncResultFunction BeginExecuteReader( _        ByVal callback As AsyncCallback, _        ByVal stateObject As Object) _             As IAsyncResultFunction BeginExecuteReader( _        ByVal behavior As CommandBehavior) _             As IAsyncResultFunction BeginExecuteReader()              As IAsyncResult

Use this method to execute T-SQL statements and return a SqlDataReader.

Note: For asynchronous executions, return the SqlCommand in the AsyncState and perform the appropriate cast to get the return type.

Use the callback parameter to register a method to be called after the asynchronous call completes.

The stateObject parameter is the object that will be returned when you call IAsyncResult.AsyncState.

The behavior parameter is a standard CommandBehavior enumeration value just as you’d use in the current version of .NET


Function BeginExecuteNonQuery( _        ByVal callback As AsyncCallback, _        ByVal stateObject As Object) _             As IAsyncResultFunction BeginExecuteNonQuery() _             As IAsyncResult

Use this to perform NonQuery operations and return the number of rows affected.

All the other overload parameters are the same as already discussed.


Function BeginExecuteXmlReader( _        ByVal callback As AsyncCallback, _        ByVal stateObject As Object) _             As IAsyncResultFunction BeginExecuteXmlReader() _              As IAsyncResult

Use this overload to make an asynchronous query that returns an XmlReader object.

All the other overload parameters are the same as already discussed.

The sample code implements both asynchronous and synchronous models of execution to bind data into the DataGridView controls. The sample also explains how to bind the controls back from the worker thread and much more in terms of error handling, etc.

Figure 2. Default Form: The figure shows how the default form should look after adding the controls described in steps 1-4.

To start,

  1. Create a new Windows Forms solution and rename it as required.
  2. Drag two DataGridView controls on to the form and rename the top and bottom grids as “dgCustomerView” and “dgOrders”.
  3. Add a status bar to provide status information.
  4. So you can compare the asynchronous and synchronous models, create two buttons to invoke the appropriate method calls. Your default form should look like Figure 2.
  5. Add a class and name it DataStore.vb. This class will return the same data in either asynchronous or synchronous mode. Here’s the method for the asynchronous data fetch within the DataStore class.
Private connectionString As String = _   "Data SOurce=INBAXPLVHARVISHNU;" + _   "Initial Catalog=Employee;" + _   "Integrated security=yes;" + _   "Asynchronous Processing=true;"   Public Function PopulateDataAsynchronously( _   ByVal query As String,    ByVal callback As AsyncCallback) As IAsyncResult   Dim conn As New SqlConnection(connectionString)   Try      conn.Open()      Dim cmd As New SqlCommand(query, conn)      Dim IAsync As IAsyncResult = _         cmd.BeginExecuteReader(callback, _         cmd, CommandBehavior.CloseConnection)      Return IAsync   Catch appex As Exception      ' handle to close the connection      If conn.State <> ConnectionState.Closed Then           conn.Close()          Throw New ApplicationException( _             "Error occured." + appex.Message, _             appex.InnerException)   End TryEnd Function

Notice that the connection string in the preceding code sets the property Asynchronous Processing=true. This ensures that the data fetch can follow an asynchronous approach when using BeginExecute; otherwise, calling BeginExecute would throw an InvalidOperationException.

The sample simply designates the connection string as a string. Alternatively, you could opt for a more readable approach using a SqlConnectionStringBuilder instance as follows:

Dim connectionString As New SqlConnectionStringBuilderconnectionString.AsynchronousProcessing = TrueconnectionString.DataSource = "Inbaxplvharvishnu"connectionString.InitialCatalog = "Employee"connectionString.IntegratedSecurity = TrueDim connstring As String = _    connectionString.ConnectionStringDim conn As New SqlConnection(connectionString)conn.Open()

Choose an Appropriate Overload
Next you choose the appropriate overload, based on your needs. In this case, the code uses BeginExecuteReader. The first parameter is an AsyncCallBack reference that invokes a registered method when query execution completes. The second parameter is the stateObject parameter (see Table 1) which will be available as AsyncState in the callback method. The third is the familiar CommandBehavior value. The call to BeginExecuteReader returns control back to the caller immediately, so that tasks on the current thread are not blocked.

Dim IAsync As IAsyncResult = cmd.BeginExecuteReader( _   callback, cmd, CommandBehavior.CloseConnection) 

From the client end the AsyncCall method invokes the server’s asynchronous method as seen below:

Dim objDataStore As New DataStoreDim asyncCustomerContext As IAsyncResult = objDataStore.PopulateDataAsynchronously( _   "Select * from Customers", _   AddressOf ProcessCustomers)Dim asyncProcessContext As IAsyncResult = _   objDataStore.PopulateDataAsynchronously( _   "Select * from Orders", AddressOf ProcessOrders)IsAsync = True

The code makes two asynchronous data calls to retrieve the Customers and Orders tables. Each call includes the appropriate callback method which ADO will invoke via delegates. Here’s an example that shows how to bind the list of customers.

Private Sub ProcessCustomers(ByVal asyncCustomer _   As IAsyncResult)   IsAsync = True   Dim dt As New DataTable()   ' Always check this;    ' otherwise you'll run into trouble   While asyncCustomer.IsCompleted      'gets the state object      Dim commandObj As SqlCommand = _         CType(asyncCustomer.AsyncState, SqlCommand)      'do the endinvoke      Dim reader As SqlDataReader = _         commandObj.EndExecuteReader(asyncCustomer)      Using reader         Try            'callback and bind on the orginal thread            dt.Load(reader)            Dim BindCustomerCallback As BindData            BindCustomerCallback = New BindData( _               AddressOf BindCustomer)            Me.Invoke(BindCustomerCallback, dt)         Catch ex As Exception            Dim throwError As New ErrorMessage( _               AddressOf ShowError)            Me.Invoke(throwError, ex.Message)         End Try      End Using   End While   IsAsync = FalseEnd Sub

The ProcessCustomers method gets an IAsyncResult instance as a parameter and uses that to retrieve the SqlCommand object that executes the EndExecuteReader method, passing in the IAsyncResult reference. That call returns a SqlDataReader object which you can load into a DataTable using the Load method. Load is an overloaded method that takes an IDataReader parameter. If the DataTable is already bound to data then it will merge the new incoming rows with the existing data; however, you can control how the DataTable merges data using the overloaded Load version that accepts a LoadOption parameter. After loading, you can use the DataTable as the data source for the DataGridView.

Figure 3. Fetching Data Asynchronously: The figure shows how the form continues to respond, displaying a status message and letting users continue to interact with the Customer grid on the form even while the code performs an asynchronous data call to get the Orders data.

The issue you now have is that the callback method now runs on a worker thread while a different thread owns the DataGrid control. You must ensure that controls on your form are accessed only from the thread which created them. Fortunately, one easy way to do that is to use the Me.Invoke method. Me.Invoke accepts a delegate that it calls in the control’s execution context?which means you can now bind the DataTable to the DataGridView by using the following delegate declaration in the form.

Private Delegate Sub BindData(ByVal dt As DataTable)Private Delegate Sub ErrorMessage( _   ByVal message As String) 

In addition to the BindData delegate the class declares another delegate, called ErrorMessage. The ErrorMessage delegate accepts a string parameter used to display error messages. Both delegates point to appropriate method references to bind the data or show the error messages. The sample code uses a similar model to bind Order information to a DataGridView and handle error messages.

For comparison, the SyncCall method in the sample code implements a synchronous mechanism to load the DataGridView. You should run both the asynchronous and synchronous models to get a feel for the difference in the interface’s responsiveness. In asynchronous mode, users will see a friendly status bar message as the first DataGridView loads (see Figure 3). However, using the synchronous mode, the user window stops responding until both the DataGridViews are bound. Further, in synchronous mode you can’t use the status bar, because the form can’t display status messages until the execution context returns.

The IsAsync variable prevents the asynchronous task from being re-executed while an existing task is pending completion. For complex database operations you need to make sure you’re using proper locking techniques while using the SqlCommand.BeginInvoke or EndInvoke methods. Also note that you must always call EndInvoke on any SqlCommand object instance where you’ve already called BeginInvoke call before using the same SqlCommand object for a different query.

Asynchronous Background Processing

Author Note’s: You’ll find the sample code for this section in the BackGroundProcessing.sln project in the downloadable source.

In addition to asynchronous SQL query support, the .NET 2.0 framework provides a new Asynchronous pattern for components supported by a set of new infrastructure classes such as AsyncOperationManager, and AsyncOperationFactory along with a set of custom delegate and event declarations. This pattern works fairly intuitively for simple tasks such as reporting execution progress, canceling already-begun tasks, etc.; however, you need to delve more deeply into the infrastructure for more advanced tasks. For these, you use the BackGroundWorker class in the System.ComponentModel namespace. BackGroundWorker runs an assigned task on a separate thread and provides a set of events and methods to start and monitor asynchronous operations, with options for querying the task status and canceling the task.

Implementing a very basic file search task in a Windows Forms application provides an ideal example, because it’s a easy to understand, fulfills a relatively common need, takes a considerable amount of time to complete?and you would want users to be able to continue working while the search is in progress. This simple application accepts an input query string, and performs a string search against a set of file names using the code FileInfo.Name.IndexOf(“input”)>0 for a set of directories, listing any matching files. Users expect such searches to operate in the background, without suspending the current application. In addition, users expect an option to stop the search at any time.

To start:

  1. Create a new Windows Forms project and open the design view of the default form.
  2. Add controls to the Form designer as shown in Figure 4.
  3. Figure 4. The File Searcher Interface: The figure shows how your default form should look after adding controls to create the File Searcher interface.
  4. Create a new class named ContentSearcher.vb, which wraps a BackGroundWorker object and contains the following code:
Private WithEvents searchFolders As _   System.ComponentModel.BackgroundWorkerPublic Delegate Sub FoundInfo(ByVal info As FileInfo)Public Delegate Sub ProcessCompleted( _   ByVal value As String)Private callback As FoundInfoPrivate callComplete As ProcessCompletedPublic Sub New(ByVal Callmethod As FoundInfo, _   ByVal CallBackOnComplete As ProcessCompleted)   searchFolders = New _      System.ComponentModel.BackgroundWorker   callback = Callmethod   Me.callComplete = CallBackOnComplete   searchFolders.WorkerReportsProgress = True   searchFolders.WorkerSupportsCancellation = TrueEnd Sub

The class declares the searchFolders BackGroundWorker instance using WithEvents to expose the object’s event model. The constructor of the ContentSearcher class has two delegate references: one to make a callback to the client when it finds a matching file, named FoundInfo, and the other, called ProcessCompleted, to return string error and completion messages. You need to set the BackgroundWorker properties WorkerReportsProgress and WorkerSupportsCancellation to True to enable progress reporting and cancellation. Table 2 lists other important BackgroundWorker methods and events.

Table 2. BackgroundWorker Members: The table lists the most important BackgroundWorker properties and methods.




This method starts the process on a separate thread asynchronously.


This method cancels a progressing background task.


On raising CancelAsync, this property value is set to True, requesting cancellation of a task.


Get/Sets a Boolean value to indicate if the worker class can support progress updates.


Get/Sets a Boolean value to indicate if the worker class can support cancellation of the current task in progress.


You can set a value to indicate the progress of the operation. The overloaded option lets you specify a userState object.


This event is invoked by calling RunWorkerAsync


Fires when the background operation is cancelled, completed, or an exception is thrown.


Fires whenever the ReportProgress property is set. The event can retrieve the userState in its overloaded parameter when set by the ReportProgress method.

Implementing BackGroundWorker Events
Here’s how the sample application implements the BackgroundWorker’s events. The DoWork event shown finds the local fixed drives and directories, and drives the recursive search.

Private Sub searchFolders_DoWork( _   ByVal sender As Object, _   ByVal e As System.ComponentModel.DoWorkEventArgs) _   Handles searchFolders.DoWork   Dim obj As System.ComponentModel.BackgroundWorker _      = CType(sender,  System.ComponentModel. _      BackgroundWorker)   Dim textToSearch As String = CType( _      e.Argument, String)   'use generics   Dim drives As System.Collections.Generic. _      ReadOnlyCollection(Of DriveInfo) = _      FileSystem.Drives   ' recurse and find the number of files so you     ' can report progress complete later   For Each drive As DriveInfo In drives      If drive.DriveType = DriveType.Fixed Then         'first level         For Each subdirectory As DirectoryInfo In _            drive.RootDirectory.GetDirectories            SetTotalFiles(subdirectory, textToSearch)         NextEnd If               Next   'approx total files is now known     'drive level   For Each value As DriveInfo In drives      If value.DriveType = DriveType.Fixed Then         Dim drive As DirectoryInfo = _            value.RootDirectory         'first level         For Each subdirectory As DirectoryInfo In _            drive.GetDirectories            Dim percentageComplete As Integer = 0            If Not obj.CancellationPending Then               SearchDirectoryRecursive(obj, e, _                  subdirectory, textToSearch)            Else               e.Cancel = True            End If         Next      End If   Next   e.Result = "Process completed."End SubPrivate totalfiles As Decimal = 0Private filesdone As Decimal = 0Private Sub SetTotalFiles(ByVal directoryInfo _   As DirectoryInfo, ByVal search As String)   ' recursively search...and set the totalfiles   Try      totalfiles = totalfiles + _         directoryInfo.GetFiles.Length      For Each info As DirectoryInfo In _         directoryInfo.GetDirectories()         totalfiles = totalfiles + _            info.GetFiles.Length         For Each subinfo As DirectoryInfo In _            info.GetDirectories()            SetTotalFiles(subinfo, search)         Next      Next   Catch ex As System.UnauthorizedAccessException      'ignore this type of error   End TryEnd Sub

The DoWork event provides a DoWorkEventArgs instance as an argument. Table 3 shows three important properties of the DoWorkEventArgs class.

Table 3. DoWorkEventArgs Properties: The table lists the three most important properties of the DoWorkEventArgs class.


Defined as an Object so it has the flexibility to accept any required type?String, Array, Hashtable XMLNode, etc. You set this by passing the parameter while calling the RunWorkerAsync method from the client. The sample code just sends the user-entered search string.


A critical property that lets you cancel the pending task. When set to True it cancels the current task and the context moves to the RunWorkerCompleted event. You use this in conjunction with the CancellationPending property to check whether the caller has issued a cancellation request.


A way to communicate the status or result to the RunWorkerCompleted method. The sample sets a string value that the UI later displays as a result message.

The code shown above fills the drives collection with a list of all drives as returned by the operating system. Note that it uses the new .NET 2.0 syntax for generics?creating a collection type that holds only DriveInfo instances. Then, it iterates through the fixed drives to get their base directories using the GetDirectories method. Next, it loops through each directory using a recursive function named SetTotalFiles. This function retrieves the total number of files to search, which is important for determining or displaying the search percentage complete.

Searching the File Set
Finally, the code makes recursive calls to a method called SearchDirectoryRecursive to find the files that match and display the same.

' Process all the files in the current directory' and get the count of files completed...filesdone = filesdone + _   directoryInfo.GetFiles().Length'report progress as of now...worker.ReportProgress(CInt(( _   filesdone / totalfiles) * 100))For Each file As FileInfo In directoryInfo.GetFiles()   If file.Name.ToLower.IndexOf( _      search.ToLower) > 0 Then      If Not worker.CancellationPending Then         callback.Invoke(file)         Threading.Thread.Sleep(10)      Else         e.Cancel = True         Exit Sub      End If   End IfNext

This method performs three important tasks. The first two lines retrieve the number of files to be searched in the current directory and call the interface to set the completed search percentage via the ReportProgress method. Note that it does this before actually processing the files, because doing such processing inside a For loop becomes complex. It’s important not to over-inflate the importance of the progress complete feature, because it just adds overhead to the intended search functionality.

The code iterates through the files, comparing each file name against the search term that the user entered. When Indexof returns a value greater than zero the file is a match, and you need to display it in the search results. In each iteration, the loop checks to see if the client has cancelled the search task by checking the BackgroundWorker.CancellationPending property. If the property value is True, the client has requested cancellation of background task, so you need to set the eventArgs.Cancel property to True, which will cancel the task. If the user hasn’t cancelled, then the code continues to the third important task: invoking the delegate using callback.Invoke (file), which passes the current file name to the method that displays search results in the ListView.

The DoWork event can complete in any of three ways:

  • The DoWork event code exits normally
  • An unhandled exception occurs
  • The user requests cancellation

No matter which way it completes, the event raises the RunWorkerCompleted event, where you can respond appropriately. For example, you can use the RunWorkerEventArgs.Cancelled property to check the user cancelled the operation, and if so return message confirming that the search was stopped. For errors, check the RunWorkerCancelEventArgs.Error property, which provides the exception details. The DoWork event also gives you access to the RunWorkerEventArgs.Result property, which can be any object that you need to return to the client.

Private Sub searchFolders_RunWorkerCompleted( _   ByVal sender As Object, _   ByVal e As System.ComponentModel. _      RunWorkerCompletedEventArgs) _   Handles searchFolders.RunWorkerCompleted  Dim obj As System.ComponentModel. _     BackgroundWorker = CType(sender, _     System.ComponentModel.BackgroundWorker)  If Not e.Error Is Nothing Then     callComplete(e.Error.Message)  ElseIf e.Cancelled Then     callComplete("Stopped the search.")  Else     callComplete(e.Result)  End IfEnd Sub

The last important event is ProgressChanged. The method accepts a ProgressChangedEventArgs argument.

Private Sub searchFolders_ProgressChanged( _   ByVal sender As Object, ByVal e As _   System.ComponentModel.ProgressChangedEventArgs) _   Handles searchFolders.ProgressChanged   progressChange = e.ProgressPercentageEnd Sub

The ProgressChangedEventArgs.ProgressPercentage property returns the percentage completed value (set in the DoWork event by calling the ReportProgress method). You can get access to the state via the ProgressChangedEventArgs.UserState property, which is an overloaded parameter of ReportProgress. This event fires each time the progress changes; therefore you should use a private variable to hold the current status. You inform the caller on the progress completion via a public Progress property.

So far, you’ve created a rich component that uses BackgroundWorker to search local fixed drives for matching file names using the BackGroundWorker class. The component can report search progress and lets users cancel the search task. It invokes delegate callbacks to registered methods passing information about matching files as FileInfo instances. Finally, it handles error messages.

Now take a look at the UI implementation.

The initial form load event creates an instance of the ContentSearcher.

Private objContentsearcher As ContentSearcherobjContentsearcher = New ContentSearcher( _   AddressOf FileInformation, AddressOf ProcessComplete) 

The first delegate references a method to display matching files, and the second report on the final search status?whether the user cancelled, an error occurred, or the process completed successfully.

You invoke the search by calling the ContentSearcher.SearchContent method.

Private Sub SearchContent(ByVal sender As _   System.Object, ByVal e As System.EventArgs) _   Handles btnSearch.Click   If Not isWorkerStarted Then      Dim a As Integer = System.Threading.Thread. _         CurrentThread.ManagedThreadId      Dim getWorker As _        System.ComponentModel.BackgroundWorker = _        objContentsearcher.Worker      lstResult.Clear()      lblProgress.Text = _         "Scanning started...please wait"      pgBar.Value = 0      pgBar.Visible = True      isWorkerStarted = True      getWorker.RunWorkerAsync(txtSearch.Text)   End IfEnd Sub

The Boolean variable isWorkerStarted ensures that users can’t perform more than one search at a time. Although you must write it each time, you should consider this technique as a standard feature; you need to implement this or a similar mechanism on the UI side whenever users or events launch asynchronous calls. The method initializes some control values and calls the ContentSearcher.RunWorkerAsync method, passing the search text as a parameter, for this simple example. Note that you could just as easily pass any other type of object, for example, an XML document that contained user preferences and additional search restrictions (e.g. search all files between 10/7/2004 and 10/8/2004) When called, the BackGroundWorker executes the DoWork event on a worker thread and the UI remains responsive, so you can display messages and search progress.

Displaying Search Results and Progress
When a file matches the search criteria, the BackGroundWorker class invokes the registered FoundInfo delegate, which fires the FileInformation method on the form. The FileInformation method receives a FileInfo object. Because FileInformation executes in the worker thread context, it calls the Form’s Invoke method to delegate the call to the Form’s thread context. The Form invokes the SetListValue method (now on the proper thread), which adds the file’s information to the ListView control. It also reads the BackgroundWorker’s progress and sets the lblProgress (label) and pgBar(progress bar) values. In other words, on each callback the code updates the progress value and adds new file information.

Private Delegate Sub SetValue(ByVal file As Object)Public Sub FileInformation(ByVal file As FileInfo)   Try      Dim handle As New SetValue( _         AddressOf Me.SetListValue)      Dim param1 As Object() = New Object() {file}      Me.Invoke(handle, param1)   Catch ex As Exception      objContentsearcher.Worker.CancelAsync()   End TryEnd SubSub SetListValue(ByVal fle As Object)   Try      Dim file As FileInfo = CType(fle, FileInfo)      Dim lv1 As ListViewItem = New _         ListViewItem(file.Name)      Dim lv2 As ListViewItem = New _         ListViewItem(file.Directory.FullName)      lstResult.Items.Add(lv1)      lstResult.Items.Add(lv2)      Dim progressCompleted As Short = _         objContentsearcher.Progress      lblProgress.Text = "Progress completed = " + _         progressCompleted.ToString + "%."      pgBar.Value = progressCompleted   Catch ex As Exception      objContentsearcher.Worker.CancelAsync()   End TryEnd Sub
Figure 5. Sample Search Results: The figure shows matching file results in the sample application form while performing a search for files whose names contain the search text “word.” The progress bar shows that the application has completed approximately one-third of the search.

At the end of the search, the BackgroundWorker makes a call to the method registered as the ProcessCompleted delegate?in this case, the ProcessComplete method shown below, passing either the error, success, or user cancellation message to display on the form.

Private Sub ProcessComplete(ByVal value As String)   lblProgress.Text = value   pgBar.Visible = False   isWorkerStarted = Not isWorkerStartedEnd Sub

Figure 5 shows how the search application looks after performing a search.

Fetching data asynchronously can go a long way toward improving the responsiveness of your Windows Forms and SmartClient applications; however, don’t simply assume that the Asynchronous model is always the best approach. Frequently, the synchronous model is a much better choice. It is nice to have the choice, though, and .NET 2.0 simplifies the programming tasks, no matter which approach you decide to use. The .NET 2.0 framework extends asynchronous support far more widely than discussed in this article. For example, if the built-in BackgroundWorker doesn’t meet your needs, you can create custom classes that support asynchronous operations. You should explore the controls you use to see which ones expose this pattern.


About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist