Пример #1
0
        public static Brush GetBackground(QuestionViewModel q)
        {
            if (q == null)
            {
                // For some reason, this appears to happen infrequently, so handle it "gracefully"
                //throw new ArgumentNullException("q");

                return Brushes.Plum;  // An ugly color on purpose
            }

            ImageBrush result;

            if (QuestionControl.mBackgroundBrushes.TryGetValue(q.State, out result))
            {
                return result;
            }

            // <ImageSource x:Key="CorrectQuestionPointsBackgroundImage">images/led_square_green.png</ImageSource>
            object value = QuestionControl.mImagesDictionary[q.State.ToString() + "QuestionPointsBackgroundImage"];

            ImageSource source = (ImageSource)value;

            result = new ImageBrush(source);

            QuestionControl.mBackgroundBrushes[q.State] = result;

            return result;
        }
Пример #2
0
 private void AddAnswersColumnControls(QuestionViewModel question)
 {
     for (int i = 0; i < 6; ++i)
     {
         AnswersColumnControl acc = new AnswersColumnControl();
         acc.DataContext = question.BeerpigRankedAnswers[i];
         Grid.SetRow(acc, 1);
         Grid.SetColumn(acc, i);
         this.mAnswersGrid.Children.Add(acc);
     }
 }
Пример #3
0
        private void HandleDesignTimeView()
        {
            HourViewModel hour = new HourViewModel(3);

            List<QuestionViewModel> qs = new List<QuestionViewModel>();

            QuestionViewModel q = new QuestionViewModel(
                new Question(69, 5)
                );
            q.Text = "Design: How much wood could a woodchuck chuck?";
            q.Points = 100;

            hour.Questions.Add(q);

            QuestionViewModel q2 = new QuestionViewModel(
                new Question(69, 6)
                );
            q2.Text = "Design: What's my name?";
            q2.Points = 50;

            hour.Questions.Add(q2);

            this.DataContext = qs;
        }
Пример #4
0
        public static void ShowAppropriateToast(HourViewModel currentHour, QuestionViewModel oldQuestionState, Question currentQuestionState)
        {
            if (oldQuestionState.State == currentQuestionState.State)
            {
                Debug.WriteLine(string.Format("No toast displayed because oldQuestionState.{0} == currentQuestionState.{1}", oldQuestionState.State, currentQuestionState.State));
                return;  // If the state didn't change, don't show a toast
            }

            if (currentQuestionState.State == QuestionState.Open)
            {
                Debug.WriteLine(string.Format("No toast displayed because currentQuestionState.{0} == QuestionState.Open", currentQuestionState.State));
                return;  // no need to show toast
            }

            string imageKey;
            string text;
            ToastNotification.TriviaIndication indication;

            if (currentQuestionState.State == QuestionState.Wrong)
            {
                if (oldQuestionState.IsBeingResearched == false)
                {
                    Debug.WriteLine(string.Format("No toast displayed because oldQuestionState.IsBeingResearched({0}) == false", oldQuestionState.IsBeingResearched));
                    return;  // Don't show any toast if a question's closed that you weren't working on
                }

                // Just closed, didn't get an answer
                text = string.Format("Question #{0} Now Closed.", currentQuestionState.Identifier.Number);

                indication = ToastNotification.TriviaIndication.IncorrectQuestion;

                imageKey = GetImageForQuestion(currentQuestionState.Identifier.Hour, currentQuestionState.Identifier.Number, ToastFactory.WrongImages);

                // If it's a high-pointer, let's show a special meme instead
                if (currentQuestionState.Points >= 80)
                {
                    imageKey = GetImageForQuestion(currentQuestionState.Identifier.Hour, currentQuestionState.Identifier.Number, ToastFactory.HighPointWrongImages);
                }
            }
            else if (currentQuestionState.State == QuestionState.Correct)
            {
                // Always show toast for correct questions (maybe in the future we should have a threshold)

                text = string.Format("Got Correct Answer for #{0} - {1} points!", currentQuestionState.Identifier.Number, currentQuestionState.Points);
               
                indication = ToastNotification.TriviaIndication.CorrectQuestion;

                imageKey = GetImageForQuestion(currentQuestionState.Identifier.Hour, currentQuestionState.Identifier.Number, ToastFactory.CorrectImages);
            }
            else
            {
                throw new InvalidOperationException("Toast in wrong place unexpectedly");
            }

            ImageSource image = (ImageSource)ToastFactory.mImagesDictionary[imageKey];

            bool shouldShowStopResearching = oldQuestionState.IsBeingResearched;

            ToastNotification toast = new ToastNotification(image);
            toast.Indication = indication;
            toast.BodyText = text;

            toast.Show();

            // Extra toasts for special situations?

            if (currentQuestionState.Identifier.Number == 9 &&
                currentHour.OpenQuestions.Count == 0 &&
                currentHour.Questions.All(q => q.State == QuestionState.Correct))
            {
                ToastNotification allofthethings = new ToastNotification((ImageSource)ToastFactory.mImagesDictionary["allofthethings"], false);
                allofthethings.Indication = indication;
                allofthethings.BodyText = "Perfect Hour";

                allofthethings.Show();
            }

            // If it's a low pointer that we missed, add an extra toast meme (only show the people researching though to avoid spam)
            if (currentQuestionState.State == QuestionState.Wrong &&
                currentQuestionState.Points < 20)  // Less than 20 points we'll consider small
            {
                ToastNotification fuckitbillmurray = new ToastNotification((ImageSource)ToastFactory.mImagesDictionary["fuckitbillmurray"], false);
                fuckitbillmurray.Indication = indication;
                fuckitbillmurray.BodyText = "Small Question Missed";

                fuckitbillmurray.Show();
            }

            // Always last/leftmost
            if (shouldShowStopResearching)
            {
                ToastNotification stopResearchingToast = new ToastNotification((ImageSource)ToastFactory.mImagesDictionary["StopResearching"], false);
                stopResearchingToast.Indication = indication;
                stopResearchingToast.BodyText = "Stop Researching This Question";

                stopResearchingToast.Show();
            }
        }
Пример #5
0
        public EditQuestionViewModel(QuestionViewModel q, bool isOpeningQuestion)
        {
            Contract.Requires(q != null);

            this.Question = q;
            this.IsOpeningQuestion = isOpeningQuestion;
        }
Пример #6
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;
            
        }
Пример #7
0
        public EditQuestionDialog(ClientOrchestrator orchestrator, QuestionViewModel question, bool isOpeningQuestion)
            : this()
        {
            Contract.Requires(orchestrator != null);
            Contract.Requires(question != null);

            this.mQuestion = question;

            this.mSnapshotOfQuestionPriorToEdits = question.CreateModel();

            this.mViewModel = new EditQuestionViewModel(question, isOpeningQuestion);

            this.mOrchestrator = orchestrator;
            this.mOrchestrator.StartAudioTriviaRecordingResponseReceived += new ClientOrchestrator.StartAudioTriviaRecordingEventHandler(mOrchestrator_StartAudioTriviaRecordingResponseReceived);
            this.mOrchestrator.StopAudioTriviaRecordingResponseReceived += new ClientOrchestrator.StopAudioTriviaRecordingEventHandler(mOrchestrator_StopAudioTriviaRecordingResponseReceived);
            
            this.DataContext = this.mViewModel;

            this.mQuestionTextTextbox.Text = question.Text;  // Because we don't want other people's updates to change the textbox, binding is OneWayToSource, so we need to set the initial value

            // If it starts with the magic text, manually set this flag
            isOpeningQuestion |= (question.Text != null && question.Text.StartsWith("Currently being opened"));

            // Set up bindings
            if (isOpeningQuestion)
            {
                // We can do this since IsOpeningQuestion will not change
                this.mAnswerPanel.Visibility = Visibility.Collapsed;
                this.mPhoneBankPanel.Visibility = Visibility.Collapsed;
                this.mStatePanel.Visibility = Visibility.Collapsed;

                question.Text = string.Empty;  // Start with empty text if opening the question

                // Special labels for opening
                this.Title = "Open Question";
                this.mHeaderActionLabel.Text = "Open Question";

                this.mRecordAudioTriviaButton.Focus();

                this.mRecordAudioTriviaButton.Click += new RoutedEventHandler(this.OnRecordingAudioTriviaClickedWhileOpeningQuestion);

                // Hack, because it's bound to a question

                RoutedEventHandler focusHandler = null;
                
                focusHandler = (sender, e) => 
                    { 
                        this.mQuestionTextTextbox.Clear(); 
                        this.mQuestionTextTextbox.GotFocus -= focusHandler;  // only once!
                    };

                this.mQuestionTextTextbox.GotFocus += focusHandler;

            }
            else
            {
                this.mAnswerPanel.SetBinding(Panel.VisibilityProperty,
                    new ConvertingBinding<QuestionState, Visibility>("Question.State",
                        state => state == QuestionState.Open ? Visibility.Collapsed : Visibility.Visible));

                this.mPhoneBankPanel.SetBinding(Panel.VisibilityProperty,
                    new ConvertingBinding<QuestionState, Visibility>("Question.State",
                        state => state == QuestionState.Correct ? Visibility.Visible : Visibility.Collapsed));

                this.mQuestionTextTextbox.Focus();
            }
            
            // Set the text for the Record button
            this.mRecordThisQuestionButtonText.SetBinding(TextBlock.TextProperty, new ConvertingBinding<string, string>("Question.AudioTriviaFilename",
                audioTriviaFileName => string.IsNullOrWhiteSpace(audioTriviaFileName) ? "Record This Question" : "Re-Record This Question"));

            
        }
Пример #8
0
 private static void HighlightAppearanceOfPartialInThisAnswer(AnswerViewModel answerViewModel, QuestionViewModel question, HashSet<string> oneWordPartials)
 {
     IEnumerable<string> words = answerViewModel.Text.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries);
     IEnumerable<string> wordsThatArePartial = words.Where(word => oneWordPartials.Contains(AnswerViewModel.GetNormalizedText(word).ToLowerInvariant()));
     answerViewModel.RelatedPartials = string.Join(", ", wordsThatArePartial.Select(word => "'" + word + "'"));
 }
Пример #9
0
 private static HashSet<string> CreateOneWordPartialsSet(QuestionViewModel question)
 {
     return new HashSet<string>(
         question.PartialAnswers
         .Cast<AnswerViewModel>()
         .Where(avm => IsOneWordAnswer(avm.Text))
         .Select(avm => AnswerViewModel.GetNormalizedText(avm.Text))
         .Select(text => text.ToLowerInvariant()));
 }
Пример #10
0
        /// <summary>
        /// A question may not always exist, for example, if someone submits an answer for an hour that's not loaded from the server yet (i.e. a past hour).  So try to get the hour if it exists.
        /// </summary>
        public bool TryGetQuestion(int hourNumber, int questionNumber, out QuestionViewModel question)
        {
            Contract.Requires(hourNumber >= 1);
            Contract.Requires(questionNumber >= 1);

            // SingleOrDefault is intentional, we might not find it...
            question = this.GetHour(hourNumber).Questions.SingleOrDefault(q => q.Identifier.Number == questionNumber);

            return question != null;
        }
Пример #11
0
        /// <summary>
        /// A question may not always exist, for example, if someone submits an answer for an hour that's not loaded from the server yet (i.e. a past hour).  So try to get the hour if it exists.
        /// </summary>
        public bool TryGetQuestion(QuestionId id, out QuestionViewModel question)
        {
            Contract.Requires(id != null);

            return this.TryGetQuestion(id.Hour, id.Number, out question);
        }
Пример #12
0
        /// <summary>
        /// A question may not always exist, for example, if someone submits an answer for an hour that's not loaded from the server yet (i.e. a past hour).  So try to get the hour if it exists.
        /// </summary>
        public bool TryGetQuestion(Answer answerToFindQuestionFor, out QuestionViewModel question)
        {
            Contract.Requires(answerToFindQuestionFor != null);

            return this.TryGetQuestion(answerToFindQuestionFor.Hour, answerToFindQuestionFor.Number,  out question);
        }
Пример #13
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);
            };
        }