Thursday, April 14, 2011

Please help me solve my W3C validation API timeout issue

I'm using the W3C validation service to check that the text I type into a TextBox is valid markup.

Valid

It's almost working. But, under particular conditions my input results in an error and then endless timeout exceptions. I have to close an re-open the program to get it working again.

Please glance over my code and help me to solve this issue.

I've got a pretty simple WPF application with a TextBox and a StatusBar. The StatusBar updates as I type to let me know if my typed markup is or is not valid. So that I'm not hammering the service, validations occur only after one second or longer has elapsed with no keystrokes.

Invalid

It StatusBar may show: "Validating...", "Valid", "Invalid", or--if there's been one--an exception's message.

Validating

The following validates successfully:

XHTML Input

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xml:lang="en">
    <head>
        <title>Test</title>
    </head>
    <body>
        <h1>Test</h1>
        <p>This is a test</p>
    </body>
</html>

If I break my paragraph like <p>This is a test</ then I get this exception while trying to process the response XML:

Name cannot begin with the '"' character, hexadecimal value 0x22. Line 86, position 40.

XML Exception

If validation fails like that twice in a row, then it seems I can't just fix my paragraph tags and continue on like normal. For some reason each subsequent validation fails with this exception:

The operation has timed out

Timed Out

This is very strange.

I'm sorry to post my whole project, but I don't know where my problem is coming from. It might be my threading, web service communication, exception handling... I just can't seem to find it. Am I closing my StreamWriter, HttpWebRequest, and ResponseStreams correctly?

XAML

<Window x:Class="Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="W3C Validation"
        Height="300"
        Width="300"
        Name="Window1">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <TextBox Grid.Row="0"
                 TextWrapping="Wrap"
                 AcceptsReturn="True"
                 VerticalScrollBarVisibility="Visible"
                 FontFamily="Consolas"
                 TextChanged="TextBox_TextChanged" />
        <StatusBar Grid.Row="1">
            <StatusBarItem>
                <TextBlock x:Name="TextBlockResult" />
            </StatusBarItem>
        </StatusBar>
    </Grid>
</Window>

Visual Basic

Imports System.ComponentModel
Imports <xmlns:env="http://www.w3.org/2003/05/soap-envelope">
Imports <xmlns:m="http://www.w3.org/2005/10/markup-validator">

Class Window1

 Private WithEvents Worker As BackgroundWorker
 Private _WorkerArgument As String

 Private Sub Window1_Loaded(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles MyBase.Loaded
  InitializeWorker()
 End Sub

 Private Sub InitializeWorker()
  Worker = New BackgroundWorker
  Worker.WorkerSupportsCancellation = True
  AddHandler Worker.DoWork, AddressOf Worker_DoWork
  AddHandler Worker.RunWorkerCompleted, AddressOf Worker_RunWorkerCompleted
 End Sub

 Private Sub TextBox_TextChanged(ByVal sender As System.Object, ByVal e As System.Windows.Controls.TextChangedEventArgs)
  TryToWork(DirectCast(sender, TextBox).Text)
 End Sub

 Sub TryToWork(ByVal Argument As String)

  If _WorkerArgument IsNot Nothing Then
   _WorkerArgument = Argument
   Exit Sub
  End If

  If Not Worker.IsBusy Then
   TextBlockResult.Text = "Validating..."
   Worker.RunWorkerAsync(Argument)
   Exit Sub
  End If

  _WorkerArgument = Argument
  Worker.CancelAsync()
  Dim RetryTimer As New Windows.Threading.DispatcherTimer
  AddHandler RetryTimer.Tick, AddressOf RetryTicker
  RetryTimer.Interval = New TimeSpan(1) '1 tick'
  RetryTimer.Start()

 End Sub

 Sub RetryTicker(ByVal sender As Object, ByVal e As System.EventArgs)
  If Not Worker.IsBusy Then
   DirectCast(sender, Windows.Threading.DispatcherTimer).Stop()
   TextBlockResult.Text = "Validating..."
   Worker.RunWorkerAsync(_WorkerArgument)
   _WorkerArgument = Nothing
  End If
 End Sub

 Private Sub Worker_DoWork(ByVal sender As Object, ByVal e As DoWorkEventArgs)
  'wait for one second'
  Dim StartTime As DateTime = DateTime.Now()
  While Now.Subtract(StartTime) < New TimeSpan(0, 0, 1)
   If DirectCast(sender, BackgroundWorker).CancellationPending Then
    e.Cancel = True
    Exit Sub
   End If
   System.Threading.Thread.Sleep(New TimeSpan(0, 0, 0, 0, 100)) 'tenth of a second'
  End While
  'then validate'
  e.Result = Validate(DirectCast(e.Argument, String))
 End Sub

 Private Function Validate(ByVal Text As String) As String
  Try
   Dim Url As String = "http://validator.w3.org/check"
   Dim Post As String = "&fragment=" + Web.HttpUtility.UrlEncode(Text) + "&output=soap12"
   Dim ResponseDocument As XDocument = XDocument.Load(New Xml.XmlTextReader(Communicate(Url, Post)))
   If ResponseDocument.Root.<env:Body>.<m:markupvalidationresponse>.<m:validity>.Value = "true" Then
    Return "Valid"
   Else
    Return "Invalid"
   End If
  Catch ex As Exception
   Return ex.Message
  End Try
 End Function

 Private Function Communicate(ByVal Url As String, ByVal Post As String) As System.IO.Stream
  Dim Writer As System.IO.StreamWriter = Nothing
  Dim Request As System.Net.HttpWebRequest = System.Net.WebRequest.Create(Url)
  Request.Method = "POST"
  Request.ContentLength = Post.Length
  Request.ContentType = "application/x-www-form-urlencoded"
  Request.Timeout = 2000 '2 seconds'
  Try
   Writer = New System.IO.StreamWriter(Request.GetRequestStream())
   Writer.Write(Post)
  Catch
  Finally
   If Not Writer Is Nothing Then
    Writer.Close()
   End If
  End Try
  Return Request.GetResponse.GetResponseStream()
 End Function

 Private Sub Worker_RunWorkerCompleted(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs)
  If Not e.Cancelled Then
   TextBlockResult.Text = DirectCast(e.Result, String)
  End If
 End Sub

End Class

Thanks for any help!

From stackoverflow
  • On a first glance, I would recomend not trying to validate every time the text is changed. That can, and does, get a bit overwhelming. I fear that is what is going on here. Good job on making it Async as that will keep it from blocking but I also suspect it is hiding your issue.

    Try having the TextChanged event set a flag to execute the validator and then just have the DoWork method run in a tight(ish) loop checking that flag. That will allow it time to complete and not get confused. It might take a second or two to get the correct data but shouldn't lock up.

    Good luck.

    bendewey : Additionally you could get blocked if you do this. It says in their docs http://validator.w3.org/docs/api.html that excessive use will get blocked.
    Zack Peterson : I've built in a 1-second-or-greater delay between each validation per their suggestion.

0 comments:

Post a Comment