在调用 GetResponseAsync() 期间收到 InvalidOperationException?

Getting InvalidOperationException during call to GetResponseAsync()?

提问人:Robert Oschler 提问时间:5/21/2013 最后编辑:Robert Oschler 更新时间:5/21/2013 访问量:1198

问:

前言:我知道代码摘录很长,但我不想遗漏其他人可能发现的问题原因的细节。代码和许多 Exception 陷阱有些冗长的原因是由于我在下面描述的对 NullReferenceException 的搜寻。通过跳转到在相互调用的异步方法中找到的 await 关键字,可以快速浏览代码到重要部分。

更新:发生 InvalidOperationException 是因为我正在更改某些按钮的 IsEnabled 状态。我在主线程上,所以我不确定为什么会发生这种情况。有谁知道为什么?

我有一个编写 C# 的 Windows Phone 7 应用程序,当在特定代码上下文中调用 GetResponseAsync() 时,该应用程序收到 System.InvalidOperationException。该应用程序使用 PetFinder API 创建一个猫品种猜谜游戏,旨在帮助动物收容所的猫被收养。以下是完整的异常消息:

Message: An unhandled exception of type 'System.InvalidOperationException' occurred in System.Windows.ni.dll

在异常发生之前,对 GetResponseAsync() 有几次成功的调用。我已按照下面调用它们的顺序包含了 Exception 中涉及的方法的代码。有人可以告诉我为什么我收到这个异常以及如何解决它吗?

异常完全发生在调用它的当前代码上下文之外,因此下面的代码与包含 GetResponseAsync() 的库之间的一些交互正在为问题创建条件。在调用 GetResponseAsync() 之前的线程代码上下文是主线程

背景说明:

这一切都始于我在 doLoadRandomPet() 中跟踪调用 getRandomPetExt() 期间发生的 NullReferenceException。根据我在 SO 上所做的阅读,我的猜测是从 getRandomPetExt() 返回了一个 NULL 任务。但是,如果你看一下这段代码,你就会发现我正在尽我所能来捕获一个虚假的异常,并避免返回一个空任务。我目前仍然认为 NULL 任务正在发生,因为其他一些代码在我的代码之外生成了虚假的异常。也许Microsoft.Bcl.Async中的某些内容?这是一些奇怪的同步上下文问题还是隐藏的跨线程访问问题?

奇怪的是,在我进行特定更改之前,我根本没有收到 InvalidOperationException,只有间歇性 NullReferenceException,每 1 次调用中每 20 到 30 次调用一次,如下所示。另一方面,每次使用新的代码结构时都会发生 InvalidOperationException。我所做的更改对我来说是一个很小的更改,旨在帮助我的调试工作。我唯一做的就是创建一个方法包装器,将 loadRandomPet() 的内脏移动到 doLoadRandomPet() 中。我这样做是为了禁用一些触发方法调用的按钮,这些按钮可能会干扰获取随机宠物的操作。我将对 doLoadRandomPet() 的调用包装在 try/finally 块中,以确保在操作退出时重新启用按钮。为什么这会导致代码执行发生如此重大的变化?

    async private void loadRandomPet(int maxRetries = 3)
    {
        // Do not allow the Guess My Breed or Adopt Me buttons to be 
        //  clicked while we are getting the next pet.
        btnAdoptMe.IsEnabled = false;
        btnGuessMyBreed.IsEnabled = false;

        try
        {
            await doLoadRandomPet(maxRetries);
        }
        finally
        {
    // >>>>> THIS CODE IS NEVER REACHED.

            // Make sure the buttons are re-enabled.
            btnAdoptMe.IsEnabled = true;
            btnGuessMyBreed.IsEnabled = true;
        }
    }

    // -------------------- CALLED NEXT

    /// <summary>
    /// (awaitable) Loads a random pet with a limit on the number of retries in case of failure.
    /// </summary>
    /// <param name="maxRetries">The number of retries permitted.</param>
    async private Task doLoadRandomPet(int maxRetries = 3)
    {
        // Show the busy indicator.
        radbusyMain.Visibility = Visibility.Visible;

        try
        {

            // Get a random pet.
            List<KeyValuePair<string, string>> listUrlArgs = new List<KeyValuePair<string, string>>();

            // Only cats.
            listUrlArgs.addKVP("animal", PetFinderUtils.EnumAnimalType.cat.ToString());

            if (!String.IsNullOrWhiteSpace(MainMenu.ZipCode))
            {
                listUrlArgs.addKVP(PetFinderUtils.URL_FIELD_LOCATION, MainMenu.ZipCode);
            }

            if (maxRetries < 0)
                throw new ArgumentOutOfRangeException("The maximum retries value is negative.");

            Debug.WriteLine("------------------ START: LOADING Random Pet ----------------");

            // Loop until a random pet is found.
            int numRetries = 0;

            // Select the breed, otherwise we will get a ton of "Domestic Short Hair" responses,
            //  which are not good for the game.  Breeds that are returning empty search
            //  results this session are filtered too.
            string strBreedName = MainMenu.GetRandomBreedName();

            listUrlArgs.addKVP("breed", strBreedName);

            while (numRetries <= maxRetries)
            {
                try
                {
                    // Save the last successful retrieval.
                    if (this._pbi != null)
                        _pbiLast = this._pbi;

                    this._pbi = await getRandomPetExt(listUrlArgs);
                }
                catch (EmptySearchResultsException esr)
                {
                    // getRandomPetExt() could not find a suitable cat given the current parameters.
                    //  Swallow the Exception without notifying the user.  Just allow the code
                    //  further down to re-use the last cat retrieved in the hopes the next
                    //  quiz won't have the problem.  
                    Debug.WriteLine(">>>>>>>>>> doLoadRandomPet() - getRandomPet() failed to find a cat.");
                }
                catch (PetFinderApiException pfExc)
                {
                    if (pfExc.ResponseCode == PetFinderUtils.EnumResponseCodes.PFAPI_ERR_LIMIT)
                        // Swallow the Exception, but let the user know to stop playing for the awhile
                        //  since we have exceeded our rate limit.
                        CatQuizAux.EasyToast("The PetFinder server is busy.\nPlease try playing the game\nlater.");
                    else
                        // Swallow the Exception, but let the user know to stop playing for the awhile
                        //  since we have exceeded our rate limit.
                        CatQuizAux.EasyToast("The PetFinder may be down.\nPlease try playing the game\nlater.");

                    // Just exit.
                    return;
                } // try
                catch (Exception exc)
                {
                    // This is really bad practice but we're out of time.   Just swallow the Exception
                    //  to avoid crashing the program.
                    Debug.WriteLine(">>>>>>>>>> doLoadRandomPet() - getRandomPet() Other Exception occurrred.  Exception Message: " + exc.Message);
                }


                // If the returned pet is NULL then no pets using the current search criteria
                //  could be found.
                if (this._pbi != null)
                {
                    // Got a random pet, stop looping.  Save it to the backup cat field too.
                    break;
                }
                else
                {
                    // Are we using a location?
                    if (listUrlArgs.hasKey(PetFinderUtils.URL_FIELD_LOCATION))
                        // Retry without the location to open the search to the entire PetFinder API
                        //  inventory.
                        listUrlArgs.deleteKVP(PetFinderUtils.URL_FIELD_LOCATION);
                    else
                    {
                        // Use a differet breed.  Add the current breed to the list of breeds returning
                        //  empty search results so we don't bother with that breed again this session.
                        MainMenu.ListEmptyBreeds.Add(strBreedName);

                        // Remove the current breed.
                        listUrlArgs.deleteKVP("breed");

                        // Choose a new breed.
                        strBreedName = MainMenu.GetRandomBreedName();
                        listUrlArgs.addKVP("breed", strBreedName);
                    } // else - if (listUrlArgs.hasKey(PetFinderUtils.URL_FIELD_LOCATION))
                } // if (this._pbi == null)

                // Sleep a bit.
                await TaskEx.Delay(1000);

                numRetries++;
            } // while (numRetries <= maxRetries)

            // If we still have a null _pbi reference, use the back-up one.
            if (this._pbi == null)
                this._pbi = this._pbiLast;

            if (this._pbi == null)
                throw new ArgumentNullException("(ViewPetRecord::doLoadRandomPet) Failed completely to find a new cat for the quiz.  Please try again later.");

            // Add the pet to the already quizzed list.
            MainMenu.AddCatQuizzed(this._pbi.Id.T.ToString());

            // Show the cat's details.
            lblPetName.Text = this._pbi.Name.T;
            imgPet.Source = new BitmapImage(new Uri(this._pbi.Media.Photos.Photo[0].T, UriKind.Absolute));

            // Dump the cat's breed list to the Debug window for inspection.
            dumpBreedsForPet(this._pbi);
        }
        finally
        {
            // Make sure the busy indicator is hidden.
            radbusyMain.Visibility = Visibility.Collapsed;
        }
    } // async private void doLoadRandomPet(int maxRetries = 3)

    // -------------------- CALLED NEXT

    /// <summary>
    /// Gets a Random Pet.  Retries up to maxRetries times to find a pet not in the already <br />
    ///  quizzed list before giving up and returning the last one found.  Also skips pets without <br />
    ///  photos.
    /// </summary>
    /// <param name="listUrlArgs">A list of URL arguments to pass add to the API call.</param>
    /// <param name="maxRetries">The number of retries to make.</param>
    /// <returns>The basic info for the retrieved pet or NULL if a pet could not be found <br />
    ///  using the current URL arguments (search criteria).</returns>
    async private Task<PetBasicInfo> getRandomPetExt(List<KeyValuePair<string, string>> listUrlArgs, int maxRetries = 3)
    {
        PetBasicInfo newPbi = null;

        try
        {
            newPbi = await doGetRandomPetExt(listUrlArgs, maxRetries);
        }
        catch (Exception exc)
        {
            Debug.WriteLine(">>>>>> (ViewPetRecord::getRandomPetExt) EXCEPTION: " + exc.Message);
            throw;
        } // try/catch

        return newPbi;
    } // async private void getRandomPetExt()

    // -------------------- CALLED NEXT

    // This was done just to help debug the NullReferenceException error we are currently fighting.
    //  see getRandomPetExt() below.
    async private Task<PetBasicInfo> doGetRandomPetExt(List<KeyValuePair<string, string>> listUrlArgs, int maxRetries = 3)
    {
        if (maxRetries < 0)
            throw new ArgumentOutOfRangeException("The maximum retries value is negative.");

        Debug.WriteLine("------------------ START: Getting Random Pet ----------------");

        // Loop until a random pet is found that has not already been used in the quiz or until
        //  we hit the maxRetries limit.
        int numRetries = 0;

        PetBasicInfo pbi = null;

        while (numRetries <= maxRetries)
        {
            try
            {
                pbi = await MainMenu.PetFinderAPI.GetRandomPet_basic(listUrlArgs);
            }
            catch (PetFinderApiException pfExcept)
            {
                // pfExcept.ResponseCode = PetFinderUtils.EnumResponseCodes.PFAPI_ERR_LIMIT;

                switch (pfExcept.ResponseCode)
                {
                    case PetFinderUtils.EnumResponseCodes.PFAPI_ERR_NOENT:
                        Debug.WriteLine("The PetFinder API returned an empty result set with the current URL arguments.");
                        // No results found.  Swallow the Exception and return
                        //  NULL to let the caller know this.
                        return null;
                    case PetFinderUtils.EnumResponseCodes.PFAPI_ERR_LIMIT:
                        Debug.WriteLine("The PetFinder API returned a rate limit error.");
                        // Throw the Exception.  Let the caller handler it.
                        throw;
                    default:
                        // Rethrow the Exception so we know about it from the crash reports.
                        // Other Exception.  Stop retrying and show the user the error message.
                        Debug.WriteLine("Exception during getRandomPetExt()\n" + pfExcept.ErrorMessage);
                        throw;
                } // switch()
            }

            // Does the pet have a photo?
            if (pbi.Media.Photos.Photo.Length > 0)
            {
                // Yes. Has the pet already been used in a quiz?
                if (!MainMenu.IsCatQuizzed(pbi.Id.T.ToString()))
                    // No. Success.
                    return pbi;
            } // if (pbi.Media.Photos.Photo.Length > 0)

            // Retry required.
            Debug.WriteLine(String.Format("Retrying, retry count: {0}", numRetries));

            // No photo or already used in a quiz.  Wait a little before retrying.
            await TaskEx.Delay(1000);

            // Count retires.
            numRetries++;
        } // while (numRetries <= maxRetries)

        // Unable to find a cat not already quizzed.  Just return the last retrieved.
        Debug.WriteLine("Retry count exceeded.  Returning last retreived pet.");

        // Returning NULL results in a await throwing a non-specific NullReferenceException.  
        //  Better to throw our own Exception.
        throw new EmptySearchResultsException("(ViewPetRecord::getRandomPetExt) Unable to retrieve a new random cat from the PetFinder API server.");
        // return pbi;
    } // async private PetBasicInfo doGetRandomPetExt()

    // ------------------ CALLED NEXT

    /// <summary>
    /// Returns the basic information for a randomly chosen pet of the given animal type.
    /// </summary>
    /// <param name="enAnimalType">The desired animal type to restrict the search to.</param>
    /// <returns></returns>
    async public Task<JSON.JsonPetRecordTypes.PetBasicInfo> GetRandomPet_basic(List<KeyValuePair<string, string>> urlArgumentPairs = null)
    {
        Debug.WriteLine("(GetRandomPet_basic) Top of call.");

        // If the URL Argument Pairs parameter is null, then create one.
        if (urlArgumentPairs == null)
            urlArgumentPairs = new List<KeyValuePair<string, string>>();

        // Add the "output" parameter that tells PetFinder we want the Basic information for the pet,
        //  not the ID or full record.
        urlArgumentPairs.addKVP("output", "basic");

        // Add a unique URL argument to defeat URL caching that may be taking
        //  place in the Windows Phone library or at the PetFinder API server.
        //  This defeats the problem so that a new random pet is returned
        //  each call, instead of the same one.
        long n = DateTime.Now.Ticks;

        urlArgumentPairs.addKVP("nocache", n.ToString());

        // Build the API call.
        string strApiCall =
                buildPetFinderApiUrl(METHOD_RANDOM_PET,
                urlArgumentPairs);

        Debug.WriteLine("(GetRandomPet_basic) URL for call: \n" + strApiCall);

        // Make the call.
        string strJsonReturn = await Misc.URLToStringAsyncEZ(strApiCall);

        bool bIsJsonReturnValid = false;

        try
        {
            JSON.JsonPetRecordTypes.PetRecordBasicInfo jsonPetBasic = JsonConvert.DeserializeObject<JSON.JsonPetRecordTypes.PetRecordBasicInfo>(strJsonReturn);

            // Deserialization succeeded.
            bIsJsonReturnValid = true;

            // Success code?
            // For some strange reason T is cast to an "int" here where in GetBreedList it's equivalent is cast to a string.
            int iResponseCode = jsonPetBasic.Petfinder.Header.Status.Code.T;

            if (iResponseCode != 100)
                throw new PetFinderApiException("PetFinder::GetRandomPet_basic", iResponseCode);
                // throw new Exception("(PetFinder::GetRandomPet_basic) The response document contains a failure response code: " + iResponseCode.ToString() + ":" + jsonPetBasic.Petfinder.Header.Status.Message);

            // Return the pet record basic info.
            return jsonPetBasic.Petfinder.Pet;
        }
        finally
        {
            if (!bIsJsonReturnValid)
                // Setting debug trap to inspect JSON return.
                Debug.WriteLine("JSON Deserialization failure.");

            Debug.WriteLine("(GetRandomPet_basic) BOTTOM of call.");
        } // try/finally
    }

    // -------------------- CALLED NEXT, never returns

    /// <summary>
    /// (awaitable) Simpler version of above call.  Same warnings about getting byte stream <br />
    ///  objects apply here as they do to URLtoStringAsync()
    /// </summary>
    /// <param name="stUrl"></param>
    /// <param name="reqMethod"></param>
    /// <returns></returns>
    async public static Task<string> URLToStringAsyncEZ(string strUrl, HttpRequestMethod reqMethod = HttpRequestMethod.HTTP_get)
    {
        strUrl = strUrl.Trim();

        if (String.IsNullOrWhiteSpace(strUrl))
            throw new ArgumentException("(Misc::URLToStringAsyncEZ) The URL is empty.");

        HttpWebRequest request = (HttpWebRequest)WebRequest.Create(strUrl);

        // Get the string value for the request method.
        request.Method = reqMethod.GetDescription();

    // >>>>> THIS CALL to GetResponseAsync() TRIGGERS THE EXCEPTION (see stack trace below)
        // Async wait for the respone.
        HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync();

        // Use a stream reader to return the string.
        using (var sr = new StreamReader(response.GetResponseStream()))
        {
            return sr.ReadToEnd();
        }
    }

// -------------------- STACK TRACE JUST BEFORE URLToStringAsync(), the call the triggers the exception.

>   Common_WP7.DLL!Common_WP7.Misc.URLToStringAsyncEZ(string strUrl, Common_WP7.Misc.HttpRequestMethod reqMethod) Line 1079 C#
CatQuiz.DLL!CatQuiz.PetFinderUtils.GetRandomPet_basic(System.Collections.Generic.List<System.Collections.Generic.KeyValuePair<string,string>> urlArgumentPairs) Line 441    C#
CatQuiz.DLL!CatQuiz.ViewPetRecord.doGetRandomPetExt(System.Collections.Generic.List<System.Collections.Generic.KeyValuePair<string,string>> listUrlArgs, int maxRetries) Line 55    C#
CatQuiz.DLL!CatQuiz.ViewPetRecord.getRandomPetExt(System.Collections.Generic.List<System.Collections.Generic.KeyValuePair<string,string>> listUrlArgs, int maxRetries) Line 123 C#
CatQuiz.DLL!CatQuiz.ViewPetRecord.doLoadRandomPet(int maxRetries) Line 243  C#
CatQuiz.DLL!CatQuiz.ViewPetRecord.loadRandomPet(int maxRetries) Line 343    C#
CatQuiz.DLL!CatQuiz.ViewPetRecord.PageViewPetRecord_Loaded(object sender, System.Windows.RoutedEventArgs e) Line 355    C#
System.Windows.ni.dll!MS.Internal.CoreInvokeHandler.InvokeEventHandler(int typeIndex, System.Delegate handlerDelegate, object sender, object args)  Unknown
System.Windows.ni.dll!MS.Internal.JoltHelper.FireEvent(System.IntPtr unmanagedObj, System.IntPtr unmanagedObjArgs, int argsTypeIndex, int actualArgsTypeIndex, string eventName)    Unknown

    ======================= EXCEPTION

// -------------------- STACK TRACE when EXCEPTION occurs
>   CatQuiz.DLL!CatQuiz.App.Application_UnhandledException(object sender, System.Windows.ApplicationUnhandledExceptionEventArgs e) Line 101 C#
System.Windows.ni.dll!MS.Internal.Error.CallApplicationUEHandler(System.Exception e)    Unknown
System.Windows.ni.dll!MS.Internal.Error.IsNonRecoverableUserException(System.Exception ex, out uint xresultValue)   Unknown
System.Windows.ni.dll!MS.Internal.JoltHelper.FireEvent(System.IntPtr unmanagedObj, System.IntPtr unmanagedObjArgs, int argsTypeIndex, int actualArgsTypeIndex, string eventName)    Unknown


    // -------------------- CODE CONTEXT when EXCEPTION occurs
    // Code to execute on Unhandled Exceptions
    private void Application_UnhandledException(object sender, ApplicationUnhandledExceptionEventArgs e)
    {
        if (System.Diagnostics.Debugger.IsAttached)
        {
            // An unhandled exception has occurred; break into the debugger
            System.Diagnostics.Debugger.Break();
        }
    }
C# Windows-Phone-7 异步等待 nullreferenceexception invalidoperationexception

评论

1赞 Kevin Gosse 5/21/2013
若要获取有关错误上下文的详细信息,请尝试中断第一次机会异常的执行。为此,请在 Visual Studio 中按 ctrl + alt + e,然后选中“公共语言运行时异常”前面的框。这样,您将确切地看到异常发生的位置,并具有关联的调用堆栈。
1赞 svick 5/21/2013
“代码摘录很长,但我不想遗漏其他人可能发现是问题原因的细节。”你为什么不尝试自己简化代码,以一种仍然显示错误的方式,然后把它发布在这里呢?
2赞 svick 5/21/2013
此外,您几乎永远不应该使用方法。一个例外是,如果该方法是事件处理程序。例外的信息是什么?是通用的“由于对象的当前状态,操作无效”还是更具体的内容?async void
1赞 Kevin Gosse 5/21/2013
@RobertOschler 这很奇怪,它应该在异常时中断。您可以尝试禁用“仅我的代码”(工具 -> 选项 -> 调试 ->取消选中“仅启用我的代码”框)。禁用它后,请确保仍选中“在异常时中断”框。
1赞 Kevin Gosse 5/21/2013
@RobertOschler 您的按钮在禁用时可能会有不同的外观(灰显或其他颜色)。它解释了为什么调用可视状态管理器。至于知道为什么会触发 InvalidOperationException...您确定异常是在主线程上触发的吗?另外,异常的“Message”属性的值是什么?由于某种原因,您的代码很可能没有在 UI 线程上执行(使用 async/await 进行上下文捕获很棘手),因此会触发异常。

答: 暂无答案