Ali Gulum

Sentiment Analysis Part 3: The Interface

Ali Gulum

Sentiment Analysis, Part 3: Interface

This is the final piece of the project: the user-facing layer that brings everything together. A console app would have been the path of least resistance, but the goal here was to show that ML.NET fits naturally into real, production-style applications. The full source code is available here.

Setting Up the Project

Add a new project to the solution, select Windows Forms App, and click Next.

Name it "MovieReviews" and click Create.

With the project in place, let's clarify what it's going to do. The Interface allows users to type any movie name, fetches general information about it along with real user reviews from IMDB, and uses our trained model to classify each review as positive or negative.

To make this work, we need two additional libraries:

  • Html Agility Pack: for parsing the IMDB page and extracting review data
  • Metro Set UI: for building a cleaner interface than the default Windows Forms controls offer

Install both from NuGet Package Manager, making sure you're on the Browse tab.

Important note: Html Agility Pack is used here purely for educational purposes. This is a tutorial project: web scraping any website in a production context is something I'd strongly advise against.

We'll also need a free API key from OMDB API to fetch general movie information. Sign up, grab your key, and keep it handy: we'll need it shortly.

Project Structure

Once the helper classes and controls are in place, the project structure will look like this:

Settings.cs

A simple static class that holds the URL templates and API configuration. Replace [APIKEY] with the key you got from OMDB.

1public static class Settings
2{
3 public static string BASE_MOVIE_URL = @"https://www.imdb.com/title/{0}";
4 public static string BASE_MOVIE_URL_REVIEWS = @"https://www.imdb.com/title/{0}/reviews?ref_=tt_ov_rt";
5 public static string API_SEARCH_URL = @"https://www.omdbapi.com/?t={0}&apikey=[APIKEY]";
6}

WebHelper

The WebHelper class handles all external data fetching: searching for movies, retrieving general information, and scraping user reviews from IMDB. It relies on Html Agility Pack under the hood. Since this article isn't about HTML parsing, I won't go into the details of how it works, but the five private helper functions below give you a clear picture of what it's doing.

1#region HtmlHelpers
2
3// Gets review nodes
4private static IEnumerable<HtmlNode> GetReviewsNodes(HtmlDocument doc)
5{
6 return doc.DocumentNode.SelectNodes("//div[contains(@class, 'lister-item-content')]");
7}
8
9// Gets rating value
10private static string GetRating(HtmlNode node)
11{
12 var mainNode = node.SelectSingleNode(".//span[contains(@class, 'rating-other-user-rating')]");
13 return mainNode != null ? mainNode.ChildNodes[3].InnerHtml : "N/A";
14}
15
16// Gets title
17private static string GetTitle(HtmlNode node)
18{
19 return node.SelectSingleNode(".//a[contains(@class, 'title')]").InnerHtml;
20}
21
22// Gets username and date
23private static string[] GetUserNameAndData(HtmlNode node)
24{
25 var mainNode = node.SelectSingleNode(".//div[contains(@class, 'display-name-date')]");
26 var userNode = mainNode.SelectSingleNode(".//span[contains(@class, 'display-name-link')]");
27 var user = userNode.SelectSingleNode("a").InnerHtml;
28 var date = mainNode.SelectSingleNode(".//span[contains(@class, 'review-date')]").InnerHtml;
29 return new[] { user, date };
30}
31
32// Gets review text
33private static string GetReview(HtmlNode node)
34{
35 return node.SelectSingleNode(".//div[contains(@class, 'text show-more__control')]").InnerHtml;
36}
37
38#endregion

ReviewItem User Control

We need a custom user control called ReviewItem to display each movie review in the list. It holds the review title, date, review text, username, star rating, and the sentiment label returned by the model.

Models

MovieSearchResultModel maps the JSON response from the OMDB API. We don't use every field, but the full structure is defined for completeness.

1public class Rating
2{
3 public string Source { get; set; }
4 public string Value { get; set; }
5}
6
7public class MovieSearchResultModel
8{
9 public string Title { get; set; }
10 public string Year { get; set; }
11 public string Rated { get; set; }
12 public string Released { get; set; }
13 public string Runtime { get; set; }
14 public string Genre { get; set; }
15 public string Director { get; set; }
16 public string Writer { get; set; }
17 public string Actors { get; set; }
18 public string Plot { get; set; }
19 public string Language { get; set; }
20 public string Country { get; set; }
21 public string Awards { get; set; }
22 public string Poster { get; set; }
23 public List<Rating> Ratings { get; set; }
24 public string Metascore { get; set; }
25 public string imdbRating { get; set; }
26 public string imdbVotes { get; set; }
27 public string imdbID { get; set; }
28 public string Type { get; set; }
29 public string DVD { get; set; }
30 public string BoxOffice { get; set; }
31 public string Production { get; set; }
32 public string Website { get; set; }
33 public bool Response { get; set; }
34 public string PageUrl { get; set; }
35 public string ReviewsPageUrl { get; set; }
36}

ReviewsModel is a simple representation of a single user review.

1public class ReviewsModel
2{
3 public string Title { get; set; }
4 public string Review { get; set; }
5 public string Rating { get; set; }
6 public string User { get; set; }
7 public string Date { get; set; }
8}

Main Screen

The UI is intentionally simple: a search field, a movie info panel, and a scrollable list of reviews. Metro Set UI handles the visual polish. Check the source code or the Metro Set UI documentation for implementation details.

Main Screen Code

We start with the same _debugMode flag used in the Trainer project. This is important: both projects must have the same value. If the Trainer saves the model in Debug mode and the Interface looks for it in Release mode, it simply won't find it.

1private static readonly bool _debugMode = true;

On form load, we initialize SentimentAnalyst with the model path and call LoadTrainedModel to load the pre-trained model from disk. No retraining happens here: we're just loading what the Trainer already produced.

1private SentimentAnalyst _sentimentAnalyst;
2
3public Form1()
4{
5 InitializeComponent();
6}
7
8private void Form1_Load(object sender, EventArgs e)
9{
10 var modelDataFile = Path.Combine(GetParentDirectory(),
11 _debugMode
12 ? $@"Movie Reviews\Movie Reviews\bin\{"Debug"}\Data"
13 : $@"Movie Reviews\Movie Reviews\bin\{"Release"}\Data",
14 "model.zip");
15 SetColors();
16 _sentimentAnalyst = new SentimentAnalyst(null, modelDataFile);
17 _sentimentAnalyst.LoadTrainedModel();
18}

Helper Functions

LoadReviews iterates over the fetched reviews, passes each one to SentimentAnalyst for prediction, and renders the result as a ReviewItem control with a green background for positive and red for negative.

GetParentDirectory resolves the solution root path, consistent with what we used in the Trainer.

SetColors applies the color scheme to the UI labels.

1#region Helpers
2
3private void LoadReviews(IEnumerable<ReviewsModel> reviews)
4{
5 var top = 0;
6 reviewList.Controls.Clear();
7 if (reviews == null) return;
8
9 foreach (var review in reviews)
10 {
11 var data = new Data { Review = review.Review };
12 var status = _sentimentAnalyst.Predicate(data);
13 var reviewItem = new ReviewItem
14 {
15 Width = 485,
16 Top = top,
17 lblTitle = { Text = review.Title },
18 lblDate = { Text = review.Date },
19 lblReview = { Text = review.Review },
20 lblUser = { Text = review.User },
21 lblRank = { Text = $@"{review.Rating}/10" },
22 lblStatus = { Text = status.PredictionValue == true ? "Positive" : "Negative" }
23 };
24 reviewItem.lblStatus.BackColor = status.PredictionValue ? Color.Green : Color.DarkRed;
25 reviewList.Controls.Add(reviewItem);
26 top += 180;
27 }
28}
29
30private static string GetParentDirectory()
31{
32 var directoryInfo = Directory.GetParent(Directory.GetCurrentDirectory()).Parent;
33 if (directoryInfo?.Parent?.Parent != null)
34 return directoryInfo.Parent.Parent.FullName;
35 return string.Empty;
36}
37
38private void SetColors()
39{
40 lblInfo1.ForeColor = Color.FromArgb(170, 170, 170);
41 lblInfo2.ForeColor = Color.FromArgb(170, 170, 170);
42 // ... remaining labels follow the same pattern
43 lblPlot.ForeColor = Color.FromArgb(170, 170, 170);
44}
45
46#endregion Helpers

Analyze Button

When the user clicks the Analyze button, the app validates that a movie name has been entered, fetches the movie info from OMDB, populates the UI fields, and then calls LoadReviews with the IMDB review data. If the movie isn't found, all fields are cleared and a warning dialog is shown.

1private void btnAnalyze_Click(object sender, EventArgs e)
2{
3 if (txtMoviewName.Text == string.Empty)
4 {
5 MessageBox.Show(this, "Please type a movie name", "Warning",
6 MessageBoxButtons.OK, MessageBoxIcon.Warning);
7 txtMoviewName.Focus();
8 return;
9 }
10
11 var movieInfo = WebHelper.GetMovieGeneralInfo(txtMoviewName.Text);
12 if (movieInfo.Response)
13 {
14 lblTitle.Text = movieInfo.Title;
15 lblYear.Text = movieInfo.Year;
16 lblReleased.Text = movieInfo.Released;
17 lblRated.Text = $@"{movieInfo.Rated}/10";
18 lblRuntime.Text = movieInfo.Runtime;
19 lblGenre.Text = movieInfo.Genre;
20 lblPlot.Text = movieInfo.Plot;
21 lblLanguage.Text = movieInfo.Language;
22 lblCountry.Text = movieInfo.Country;
23 imdbRating.Text = movieInfo.imdbRating;
24 lblInfo11.Text = $@"{movieInfo.imdbRating}/10";
25 if (movieInfo.Poster != null)
26 if (movieInfo.Poster != "N/A")
27 moviePicture.Load(movieInfo.Poster);
28
29 LoadReviews(WebHelper.GetMovieReviews(movieInfo.ReviewsPageUrl));
30 }
31 else
32 {
33 lblTitle.Text = string.Empty;
34 lblYear.Text = string.Empty;
35 lblReleased.Text = string.Empty;
36 lblRated.Text = string.Empty;
37 lblRuntime.Text = string.Empty;
38 lblGenre.Text = string.Empty;
39 lblPlot.Text = string.Empty;
40 lblLanguage.Text = string.Empty;
41 lblCountry.Text = string.Empty;
42 imdbRating.Text = string.Empty;
43 moviePicture.Image = null;
44 reviewList.Controls.Clear();
45 MessageBox.Show(this, "Cannot find the movie, please check the name and try again.",
46 "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
47 txtMoviewName.Focus();
48 }
49}

Where Does ML.NET Actually Fit In?

You might have noticed that there's no direct ML.NET code anywhere in the Interface project. That's intentional: all of the machine learning logic lives in the SentimentAnalyst class from Part 1. The Interface simply calls it.

The entire prediction happens in a single line inside LoadReviews:

1var status = _sentimentAnalyst.Predicate(data);

That one call does everything: tokenizes the review, runs it through the trained model, and returns a positive or negative classification.

Results

When you run the application, type a movie name, and click Analyze, each review gets a colored label: green for positive, red for negative.

The results are impressively accurate. ML.NET handles the sentiment classification reliably, and the predictions hold up well against real user reviews pulled directly from IMDB.

That wraps up the Sentiment Analysis series. We built a complete end-to-end machine learning application in .NET: from training a model on 50,000 movie reviews to deploying it in a real Windows Forms application that classifies live user input. Next up, we'll look at building a multi-classification example.