Example #1
0
        public static void Start(ClientOrchestrator orchestrator, TriviaViewModel viewModel, CommandBindingCollection bindings, TriviaClient window)
        {
            Contract.Requires(orchestrator != null);

            bindings.Add(new CommandBinding(TriviaCommands.WrongAnswer,
                (object source, ExecutedRoutedEventArgs e) =>
                {
                    AnswerViewModel a = (AnswerViewModel)e.Parameter;

                    a.WhoCalledIn = TriviaClient.PlayerName;

                    orchestrator.SendUpdateAnswerRequest(a.CreateModel(), false);
                }));

            bindings.Add(new CommandBinding(TriviaCommands.CorrectAnswer,
                (object source, ExecutedRoutedEventArgs e) =>
                {
                    CorrectAnswerData cad = (CorrectAnswerData)e.Parameter;

                    AnswerViewModel a = cad.Answer;

                    a.WhoCalledIn = TriviaClient.PlayerName;

                    orchestrator.SendUpdateAnswerRequest(a.CreateModel(), false);

                    QuestionChanges questionChanges = new QuestionChanges(new QuestionId(a.Hour, a.Number))
                    {
                        Open = false,
                        Correct = true,
                        Answer = a.Text,
                        PhoneBankOperator = cad.PhoneBankOperator
                    };

                    // Update the correct answer

                    orchestrator.SendUpdateQuestionRequest(questionChanges);
                }));

            bindings.Add(new CommandBinding(TriviaCommands.PartialAnswer,
                (object source, ExecutedRoutedEventArgs e) =>
                {
                    AnswerViewModel a = (AnswerViewModel)e.Parameter;

                    a.WhoCalledIn = TriviaClient.PlayerName;
                    a.Partial = true;

                    orchestrator.SendUpdateAnswerRequest(a.CreateModel(), true);  // Flag as a partial
                }));

            bindings.Add(new CommandBinding(TriviaCommands.EditAnswer,
                (object source, ExecutedRoutedEventArgs e) =>
                {
                    // An edited answer was already modified by the command sender, so just update it to the server

                    // Note: this is usually done by fixing the text of a partial answer

                    AnswerViewModel a = (AnswerViewModel)e.Parameter;

                    orchestrator.SendUpdateAnswerRequest(a.CreateModel(), false);  // Flag not "partial changed".  We don't know it to be the case, but that's the behavior we want
                }));

            bindings.Add(new CommandBinding(TriviaCommands.ShowSubmitAnswerDialog,
                 (object source, ExecutedRoutedEventArgs e) =>
                 {
                     QuestionViewModel question = (QuestionViewModel)e.Parameter;

                     SubmitAnswerDialog submitAnswerDialog = new SubmitAnswerDialog(question);
                     submitAnswerDialog.Owner = window;

                     bool? submitted = submitAnswerDialog.ShowDialog();

                     if (submitted == true)
                     {
                         // 2014 - don't think this is true, but preserving the comment... Must execute the command against "this" current window so the commandbindings are hit
                         TriviaCommands.SubmitAnswer.Execute(submitAnswerDialog.AnswerSubmitted, null);  // Using "null" as the target means we avoid weird bugs where the event actually doesn't fire
                     }
                 }));

            bindings.Add(new CommandBinding(TriviaCommands.SubmitAnswer,
                (object source, ExecutedRoutedEventArgs e) =>
                {
                    Answer a = (Answer)e.Parameter;

                    QuestionViewModel q;
                   
                    if (!(viewModel.TryGetQuestion(a, out q)))
                    {
                        throw new InvalidOperationException("Couldn't get question for answer submitted, that's really odd...");
                    }

                    if (ContestConfiguration.PreventDuplicateAnswerSubmission && 
                        CommandHandler.ShouldStopAnswerSubmission(a, q, window))
                    {
                        return;
                    }

                    orchestrator.SendSubmitAnswerRequest(a);  // Inform the server

                    // If you're submitting an answer, it's likely to assume you're researching the question
                    if (!(q.IsBeingResearched))
                    {
                        q.IsBeingResearched = true;

                        TriviaCommands.ResearcherChanged.Execute(q, null);
                    }
                }));

            bindings.Add(new CommandBinding(TriviaCommands.OpenQuestion,
                (object source, ExecutedRoutedEventArgs e) =>
                {
                    QuestionId questionId = (QuestionId)e.Parameter;

                    orchestrator.SendOpenQuestionRequest(questionId.Hour, questionId.Number);
                }));

            bindings.Add(new CommandBinding(TriviaCommands.EditQuestion,
                (object source, ExecutedRoutedEventArgs e) =>
                {
                    QuestionViewModel question = (QuestionViewModel)e.Parameter;

                    EditingQuestionData data = new EditingQuestionData() 
                    { 
                        HourNumber = question.Identifier.Hour, 
                        QuestionNumber = question.Identifier.Number, 
                        WhoEditing = TriviaClient.PlayerName 
                    };

                    // Tell everyone else that "I'm editing this question, so back off!"
                    orchestrator.SendEditingQuestionMessage(data);

                    EditQuestionDialog dialog = new EditQuestionDialog(orchestrator, question, false);
                    dialog.Owner = window;

                    bool? result = dialog.ShowDialog();

                    if (!(result.HasValue) || result.Value == false)
                    {
                        data.WhoEditing = null; // Signal that the edit was cancelled
                        
                        orchestrator.SendEditingQuestionMessage(data);

                        // If the user made a change to the question (in-memory viewmodel), should refresh from the server (it's just the easiest thing to do)
                        if (dialog.GetChangesMade().Changes != QuestionChanges.Change.None)
                        {
                            orchestrator.SendGetCompleteHourDataRequest(question.Identifier.Hour);
                        }

                        return;  // Cancelled / Closed without saving
                    }

                    QuestionChanges changes = dialog.GetChangesMade();

                    if (changes.Changes == QuestionChanges.Change.None)
                    {
                        return;  // No changes actually were made even though the user clicked the Save button
                    }

                    orchestrator.SendUpdateQuestionRequest(changes);
                }));

            bindings.Add(new CommandBinding(TriviaCommands.UpdateQuestionPoints,
                (object source, ExecutedRoutedEventArgs e) =>
                {
                    // Only Beerpigs use this
                    QuestionChanges changes = (QuestionChanges)e.Parameter;

                    orchestrator.SendUpdateQuestionRequest(changes);
                }));

            bindings.Add(new CommandBinding(TriviaCommands.PlayAudioTrivia,
                (object source, ExecutedRoutedEventArgs e) =>
                {
                    string audioTriviaFileName = (string)e.Parameter;

                    CommandHandler.PlayAudioTrivia(audioTriviaFileName, window);
                }));

            bindings.Add(new CommandBinding(TriviaCommands.ShowAddNoteDialog,
                (object source, ExecutedRoutedEventArgs e) =>
                {
                    QuestionViewModel question = (QuestionViewModel)e.Parameter;

                    Note note = new Note();
                    note.Submitter = TriviaClient.PlayerName;
                    note.HourNumber = question.Identifier.Hour;
                    note.QuestionNumber = question.Identifier.Number;

                    AddNoteDialog dialog = new AddNoteDialog(note);
                    dialog.Owner = window;

                    bool? result = dialog.ShowDialog();

                    if (!(result.HasValue) || result.Value == false)
                    {
                         return;  // Cancelled / Closed without saving
                    }

                    note.Text = note.Text.Trim();  // Trim it.

                    // Save the note
                    orchestrator.SendAddNoteMessage(note);

                    // If you're adding a note, it's likely to assume you're researching the question

                    if (!(question.IsBeingResearched))
                    {
                        question.IsBeingResearched = true;

                        TriviaCommands.ResearcherChanged.Execute(question, null);
                    }
                }));

            bindings.Add(new CommandBinding(TriviaCommands.CorrectAnswerWithoutSubmission,
                (object source, ExecutedRoutedEventArgs e) =>
                {
                    QuestionViewModel question = (QuestionViewModel)e.Parameter;

                    Answer answer = new Answer();
                    answer.Hour = question.Identifier.Hour;
                    answer.Number = question.Identifier.Number;
                    answer.WhoCalledIn = TriviaClient.PlayerName;

                    AnswerViewModel answerViewModel = new AnswerViewModel(answer);

                    // Show the "blank" dialog
                    CorrectAnswerDialog dialog = new CorrectAnswerDialog(answerViewModel);
                    dialog.Owner = window;

                    bool? result = dialog.ShowDialog();

                    if (!(result.HasValue) || result.Value == false)
                    {
                        return;  // Cancelled / Closed without saving
                    }

                    QuestionChanges changes = new QuestionChanges(question.Identifier)
                    {
                        Open = false,
                        Correct = true,
                        Answer = dialog.Answer.Text,
                        PhoneBankOperator = dialog.PhoneBankOperator
                    };

                    // Update the correct answer

                    orchestrator.SendUpdateQuestionRequest(changes);
                }));

            bindings.Add(new CommandBinding(TriviaCommands.ResearcherChanged,
                (object source, ExecutedRoutedEventArgs e) =>
                {
                    QuestionViewModel questionThatChanged = (QuestionViewModel)e.Parameter;

                    if (questionThatChanged.IsBeingResearched == false)
                    {
                        // User said they're not researching this question (and by extension, aren't researching *anything* anymore)
                        orchestrator.SendResearcherChangedRequest(
                            new ResearcherChange
                            {
                                HourNumber = 0,  // Signaling that current user is researching nothing
                                QuestionNumber = 0,  // Signaling that current user is researching nothing
                                Name = TriviaClient.PlayerName
                            });
                    }
                    else
                    {
                        orchestrator.SendResearcherChangedRequest(
                          new ResearcherChange
                          {
                              HourNumber = questionThatChanged.Identifier.Hour,
                              QuestionNumber = questionThatChanged.Identifier.Number,
                              Name = TriviaClient.PlayerName
                          });
                    }
                }));


            bindings.Add(new CommandBinding(TriviaCommands.ShowEditPartialsDialog,
                (object source, ExecutedRoutedEventArgs e) =>
                {
                    ListCollectionView partials = (ListCollectionView)e.Parameter;

                    EditPartialsDialog dialog = new EditPartialsDialog();
                    dialog.DataContext = partials;
                    dialog.Owner = window;

                    dialog.ShowDialog();
                }));

            bindings.Add(new CommandBinding(TriviaCommands.ShowCombinePartialsDialog,
                (object source, ExecutedRoutedEventArgs e) =>
                {
                    QuestionViewModel question = (QuestionViewModel)e.Parameter;

                    string combinationAnswer = string.Join("\n", question.PartialAnswers.Cast<AnswerViewModel>().Select(avm => avm.Text));

                    SubmitAnswerDialog submitAnswerDialog = new SubmitAnswerDialog(question, combinationAnswer);
                    submitAnswerDialog.Owner = window;

                    bool? submitted = submitAnswerDialog.ShowDialog();

                    if (submitted == true)
                    {
                        // 2014, don't think this is true, ubt pres3erving the original comment.....  Must execute the command against "this" current window so the commandbindings are hit
                        TriviaCommands.SubmitAnswer.Execute(submitAnswerDialog.AnswerSubmitted, null);  // Using "null" as the target means we avoid weird bugs where the event actually doesn't fire
                    }
                }));


            bindings.Add(new CommandBinding(TriviaCommands.ShowAddLinkDialog,
                (object source, ExecutedRoutedEventArgs e) =>
                {
                    AddLinkDialog dialog = new AddLinkDialog();
                    dialog.Owner = window;

                    bool? result = dialog.ShowDialog();

                    if (!(result.HasValue) || result.Value == false)
                    {
                        return;  // Cancelled / Closed without saving
                    }

                    dialog.Link.Description = dialog.Link.Description.Trim();
                    dialog.Link.Url = dialog.Link.Url.Trim();

                    // Save the link
                    orchestrator.SendAddLinkRequest(dialog.Link);
                }));            
            bindings.Add(new CommandBinding(TriviaCommands.DeleteNote,
                (object source, ExecutedRoutedEventArgs e) =>
                {
                    orchestrator.SendDeleteNoteMessage((Note)e.Parameter);
                }));
        }
Example #2
0
        private static bool ShouldStopAnswerSubmission(Answer submittedAnswer, QuestionViewModel q, TriviaClient window)
        {
            string normalizedSubmittedAnswer = AnswerViewModel.GetNormalizedText(submittedAnswer.Text);

            AnswerViewModel duplicateAnswer = q.SubmittedAnswers.AsParallel().FirstOrDefault(otherAnswer => AnswerViewModel.GetNormalizedText(otherAnswer.Text).Equals(normalizedSubmittedAnswer, StringComparison.OrdinalIgnoreCase));

            if (duplicateAnswer == null)
            {
                return false;
            }

            string message;

            if (duplicateAnswer.HasBeenCalledIn)
            {
                if (duplicateAnswer.Text.Equals(q.Answer))
                {
                    message = string.Format("The answer '{0}' is already accepted as the correct answer.", duplicateAnswer.Text);
                }
                else if (duplicateAnswer.Partial)
                {
                    message = string.Format("The answer '{0}' has been identified as a partial.", duplicateAnswer.Text);
                }
                else
                {
                    message = string.Format("The answer '{0}' was already called in and was wrong.", duplicateAnswer.Text);
                }
            }
            else
            {
                message = string.Format("The answer '{0}' has already been submitted by {1} and is on the list to call in.", duplicateAnswer.Text, duplicateAnswer.Submitter);
            }

            MessageBox.Show(window, message, "Already Submitted", MessageBoxButton.OK);
            
            return true;
            
        }
Example #3
0
        public static void Start(ClientOrchestrator orchestrator, TriviaViewModel viewModel, Dispatcher dispatcher, TriviaClient window)
        {
            Contract.Requires(orchestrator != null);
            Contract.Requires(viewModel != null);

            orchestrator.AuthenticateResponseReceived +=
                delegate (AuthenticationResponse authResponse)
                {
                    // Since as of 2016, this response contains hour/player data, need to process this
                    if (authResponse.CurrentHourData != null)  // Will be null if authentication was unsuccessful
                    {
                        orchestrator.TriggerGetCompleteHourDataResponseReceivedEvent(authResponse.CurrentHourData);
                    }

                    if (authResponse.Players != null)
                    {
                        orchestrator.TriggerPlayersListUpdateReceivedEvent(authResponse.Players);
                    }

                    dispatcher.Invoke(new Action(
                        delegate
                        {
                            // HourViewModel requires being on the UI thread
                            LoadGlobalDataIntoViewModel(authResponse.GlobalData, viewModel);
                        }));
                };
                      
            // Complete Hour
            orchestrator.GetCompleteHourDataResponseReceived +=
                delegate(CompleteHourData chd)
                {
                    dispatcher.Invoke(new Action(
                        delegate
                        {
                            Debug.WriteLine("CompletHourDataResponse for hour " + chd.HourNumber.ToString());

                            HourViewModel hour = viewModel.GetHour(chd.HourNumber);

                            hour.IsNoLongerCurrentHour = chd.IsNoLongerCurrentHour;

                            hour.Questions.Clear();

                            hour.HasRetrievedQuestionsFromServer = true;  // Flag that we've got the latest & greatest

                            ILookup<int, Answer> answerLookup = chd.Answers.ToLookup(a => a.Number);
                            ILookup<int, Note> noteLookup = chd.Notes.ToLookup(n => n.QuestionNumber);

                            // Obviously this could be fancier w/ LINQ, but eh, good enough
                            foreach (Question questionModel in chd.Questions)
                            {
                                var answers = answerLookup[questionModel.Number];
                                var notes = new DispatchableObservableCollection<Note>(noteLookup[questionModel.Number]);

                                QuestionViewModel questionViewModel = new QuestionViewModel(questionModel, answers, notes);

                                hour.Questions.Add(questionViewModel);
                            }
                        }));
                };
            
            // Latest Hour (for informing the player that old hours are no longer active)
            orchestrator.GetLatestHourResponseReceived +=
                delegate(int latestHour)
                {
                    dispatcher.Invoke(new Action(
                        delegate
                        {
                            foreach (HourViewModel hour in viewModel.Hours)
                            {
                                hour.IsNoLongerCurrentHour = hour.Number < latestHour;
                            }
                        }));
                };

            // Questions

            orchestrator.OpenQuestionResponseReceived += delegate (int hourNumber, int questionNumber, bool successful, string failureMessage)
            {
                // BeginInvoke is absolutely key here, otherwise the UI thread will be tied up and messages (such as Start Audio Trivia Recording) won't
                // be able to be processed as long as the UI thread is in use.  Regular old Invoke() will cause this problem.
                dispatcher.BeginInvoke(new Action(
                        delegate
                        {
                            if (!(successful))
                            {
                                MessageBox.Show(failureMessage, string.Format("Question #{0} Was Not Opened", questionNumber), MessageBoxButton.OK, MessageBoxImage.Exclamation);
                                return;  // Nothing to do
                            }

                            HourViewModel hour = viewModel.GetHour(hourNumber);

                            // Boy, I hope that the Update Question response (which creates the question) is received before this is...
                            QuestionViewModel question;

                            if (!(viewModel.TryGetQuestion(hourNumber, questionNumber, out question)))
                            {
                                // Fake it out since we didn't get a real one from the server
                                question = new QuestionViewModel(new Question(hourNumber, questionNumber));

                                hour.Questions.Add(question);
                            }

                            EditQuestionDialog dialog = new EditQuestionDialog(orchestrator, question, true);
                            dialog.Owner = window;

                            bool? result = dialog.ShowDialog();

                            if (!(result.HasValue) || result.Value == false)
                            {
                                return;  // Cancelled / Closed without saving
                            }

                            QuestionChanges changes = dialog.GetChangesMade();

                            if (changes.Changes == QuestionChanges.Change.None)
                            {
                                return;  // No changes actually were made even though the user clicked the Save button
                            }

                            orchestrator.SendUpdateQuestionRequest(changes);
                        }));
            };

            orchestrator.UpdateQuestionResponseReceived +=
                delegate(Question updatedQuestion, bool playerInitatedUpdate)
                {
                    dispatcher.Invoke(new Action(
                        delegate
                        {
                            HourViewModel hour = viewModel.GetHour(updatedQuestion.Hour);

                            // If the question is just being opened, the first we hear of this question is the Update message

                            QuestionViewModel wrapperToUpdate;

                            if (!(viewModel.TryGetQuestion(updatedQuestion.Identifier, out wrapperToUpdate)))
                            {
                                // A brand new opened question
                                QuestionViewModel questionToAdd = new QuestionViewModel(updatedQuestion);

                                hour.Questions.Add(questionToAdd);
                                return;
                            }

                            ToastFactory.ShowAppropriateToast(viewModel.GetHour(updatedQuestion.Hour), wrapperToUpdate, updatedQuestion);

                            if (playerInitatedUpdate)  // If an automatic process updated this question (such as Predicted Duration change), a person could still be editing
                            {
                                wrapperToUpdate.PersonCurrentlyEditingThisQuestion = null;  // Flag that there's (likely) nobody else Editing this question
                            }

                            wrapperToUpdate.Update(updatedQuestion, playerInitatedUpdate);
                        }));
                };

            orchestrator.EditingQuestionMessageReceived +=
                delegate(EditingQuestionData data)
                {
                    dispatcher.Invoke(new Action(
                        delegate
                        {
                            HourViewModel hour = viewModel.GetHour(data.HourNumber);

                            // If the question is just being opened, the first we hear of this question is the Update message
                            QuestionViewModel questionBeingUpdated;

                            if (!(viewModel.TryGetQuestion(data.HourNumber, data.QuestionNumber, out questionBeingUpdated)))
                            {
                                return;  // Odd that we can't find it, but this message isn't super-important so just move on
                            }

                            questionBeingUpdated.PersonCurrentlyEditingThisQuestion = data.WhoEditing;
                        }));
                };

            // Answers

            orchestrator.UpdateAnswerResponseReceived +=
                delegate(Answer answer, bool partialChanged)
                {
                    dispatcher.Invoke(new Action(
                        delegate
                        {
                            //Debug.WriteLine(string.Format("UpdateAnswer.  Number:{0}.  IsPartial:{1}.  PartialChanged:{2}.  HasBeenCalledIn:{3}", answer.Number, answer.Partial, partialChanged, answer.HasBeenCalledIn));

                            QuestionViewModel question;

                            if (!(viewModel.TryGetQuestion(answer, out question)))
                            {
                                // If we can't find it - it's assumed to be for an hour that's not retrievd from the server yet.
                                return;  
                            }

                            if (partialChanged && answer.Partial)
                            {
                                // Toasty!
                                ToastFactory.ShowToastForPartial(answer);
                            }

                            AnswerViewModel storedAnswer = question.SubmittedAnswers.SingleOrDefault(a => a.ID == answer.ID);

                            if (storedAnswer != null)  // Wouldn't expect it to be null, but in 2014 it was null in the wild one random time
                            {
                                // Make the update
                                storedAnswer.Update(answer);
                            }

                            // If it changed partial status, or it's already a partial (and edited to change the text)
                            if (partialChanged || answer.Partial)
                            {
                                if (IsOneWordAnswer(answer.Text))
                                {
                                    // The fact that this answer changed partial status doesn't affect anything else, ignore
                                    // because we only care about one-word answers
                                    question.HighlightAppearanceOfPartialsInAnswers();
                                }
                            }
                        }));
                };

            orchestrator.SubmitAnswerResponseReceived +=
                delegate(Answer answer)
                {
                    //  viewModel.TryGetQuestion() eventually could trigger a new Hour to be created, which relies on the UI thread (Hours view)
                    dispatcher.Invoke(new Action(
                        delegate
                        {
                            QuestionViewModel question;

                            if (!(viewModel.TryGetQuestion(answer, out question)))
                            {
                                // If we can't find it - it's assumed to be for an hour that's not retrievd from the server yet.
                                return;
                            }

                            AnswerViewModel answerViewModel = new AnswerViewModel(answer);

                            question.SubmittedAnswers.Add(answerViewModel);

                            question.HighlightAppearanceOfPartialInThisAnswer(answerViewModel);

                            /*  // couldn't finish this in 2011.
                            // To keep track of how many of your personal answers are in the queue
                            viewModel.GetHour(question.Identifier.Hour).AddAnswerSubmittedByCurrentUser(answerViewModel);
                             */
                        }));
                };

            orchestrator.AddNoteMessageReceived +=
                delegate(Note note)
                {
                    QuestionViewModel question;

                    if (!(viewModel.TryGetQuestion(note.HourNumber, note.QuestionNumber, out question)))
                    {
                        // If we can't find it - it's assumed to be for an hour that's not retrievd rom the server yet.
                        return;
                    }

                    question.Notes.Add(note);
                };
            orchestrator.DeleteNoteMessageReceived +=
                delegate (int noteId)
                {
                    var allHoursStartingWithCurrent = new HourViewModel[] { viewModel.ActiveHour }.Concat(viewModel.Hours.Cast<HourViewModel>());
                    var allQuestionsStartingWithCurrentHour = allHoursStartingWithCurrent.SelectMany(hour => hour.Questions);
                    var firstMatchingNote = allQuestionsStartingWithCurrentHour.SelectMany(q => q.Notes).Where(note => note.ID == noteId).FirstOrDefault();

                    if (firstMatchingNote == null)
                    {
                        // Someone must have deleted a note that's not in the Client's cache of questions yet...
                        return;
                    }

                    QuestionViewModel question;

                    bool found = viewModel.TryGetQuestion(firstMatchingNote.HourNumber, firstMatchingNote.QuestionNumber, out question);

                    if (!(found))
                    {
                        return; // Question not currently loaded by the client yet?  No biggie, it'll get the latest values later when it is loaded
                    }

                    question.Notes.Remove(firstMatchingNote);
                };
            /* dead code?
            orchestrator.GetGlobalDataResponseReceived += (globalData) =>
                {
                    dispatcher.Invoke(new Action(
                        delegate
                        {
                            LoadGlobalDataIntoViewModel(globalData, viewModel);
                        }));
                };
                */
            orchestrator.CurrentCallerChangedMessageReceived += (currentCaller) =>
                {
                    viewModel.CurrentCaller = currentCaller;
                };

            orchestrator.ResearcherChangedResponseReceived += (changes) =>
            {
                dispatcher.Invoke(new Action(
                    delegate
                    {
                        Debug.WriteLine("ResearcherChanged response received");

                        // First, remove this researcher from any (if applicable) questions they were researching previously
                        var allHoursStartingWithCurrent = new HourViewModel[] { viewModel.ActiveHour }.Concat(viewModel.Hours.Cast<HourViewModel>());
                        var allQuestionsStartingWithCurrentHour = allHoursStartingWithCurrent.SelectMany(hour => hour.Questions);
                        var previouslyResearchedQuestion = allQuestionsStartingWithCurrentHour.Where(q => q.Researchers.Contains(changes.Name)).FirstOrDefault();

                        Debug.WriteLine(string.Format("PreviouslyRsearchedQuestion: {0}", previouslyResearchedQuestion == null ? "null" : previouslyResearchedQuestion.Identifier.Number.ToString()));

                        if (previouslyResearchedQuestion != null)
                        {
                            // They are no longer researching this question
                            previouslyResearchedQuestion.Researchers.Remove(changes.Name);

                            // If the "person" is the current user, need to uncheck the checkbox
                            if (changes.Name == TriviaClient.PlayerName)
                            {
                                previouslyResearchedQuestion.IsBeingResearched = false;
Debug.WriteLine(string.Format("Unchecking checkbox for: {0}", previouslyResearchedQuestion.Identifier.Number.ToString()));
                            }
                        }

                        QuestionViewModel question;

                        bool found = viewModel.TryGetQuestion(changes.HourNumber, changes.QuestionNumber, out question);

                        if (!(found))
                        {
                            return; // Question not currently loaded by the client yet?  No biggie, it'll get the latest values later when it is loaded
                        }

                        question.Researchers.Add(changes.Name);  // Add to end of list

                        Debug.WriteLine(string.Format("{0} added to researchers of question: {1}", changes.Name, question.Identifier.Number));

                    }), DispatcherPriority.Background); // ResearcherChanged tends to trigger some slow things, so make it a low priority since it's nonessential
            };

            // Links

            orchestrator.AddLinkResponseReceived += (link) =>
            {
                dispatcher.Invoke(new Action(
                    delegate
                    {
                        viewModel.Links.Add(link);
                    }));
            };

            // Players
            orchestrator.PlayersListUpdateReceived += (IEnumerable<Player> players) =>
            {
                dispatcher.Invoke(new Action(
                    delegate
                    {
                        
                        viewModel.ConnectedPlayers.Clear();
                        viewModel.RecentlyJoinedPlayers.Clear();
                        viewModel.RecentlyDepartedPlayers.Clear();

                        foreach (Player player in players)
                        {
                            if (player.RecentlyDeparted)
                            {
                                viewModel.RecentlyDepartedPlayers.Add(player);
                            }
                            else if (player.RecentlyJoined)
                            {
                                viewModel.RecentlyJoinedPlayers.Add(player);
                            }
                            else
                            {
                                viewModel.ConnectedPlayers.Add(player);
                            }
                        }
                    }), DispatcherPriority.Background);
            };
        }