├── .gitattributes
├── .gitignore
├── AmazonScrape.csproj
├── AmazonScrape.sln
├── App.config
├── App.xaml
├── App.xaml.cs
├── Constants.cs
├── Model
├── Logic
│ ├── ItemValidator.cs
│ ├── PageManager.cs
│ ├── Parser.cs
│ ├── Scraper.cs
│ └── SearchManager.cs
└── Structure
│ ├── AmazonItem.cs
│ ├── IValidatable.cs
│ ├── Result.cs
│ ├── ScoreDistribution.cs
│ └── SearchCriteria.cs
├── Properties
├── AssemblyInfo.cs
├── Resources.Designer.cs
├── Resources.resx
├── Settings.Designer.cs
└── Settings.settings
├── README.md
├── UI
├── Controls
│ ├── DataGridPlus.cs
│ ├── RangeBoxes.xaml
│ ├── RangeBoxes.xaml.cs
│ ├── RangeSlider.xaml
│ ├── RangeSlider.xaml.cs
│ ├── ScoreDistributionControl.cs
│ ├── ScoreDistributionControl.xaml
│ ├── ScoreDistributionControl.xaml.cs
│ ├── TextBoxPlus.xaml
│ ├── TextBoxPlus.xaml.cs
│ └── ValueRangeControl.cs
├── MainWindow.xaml
├── MainWindow.xaml.cs
└── Resources
│ ├── ControlStyles.xaml
│ ├── Images
│ ├── application_icon.ico
│ ├── icon.ico
│ └── prime_logo.png
│ └── ResourceLoader.cs
├── UtilityClasses
├── DoubleRange.cs
├── DoubleRangeConverter.cs
├── IntRange.cs
└── NumericRange.cs
└── packages.config
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.sln.docstates
8 |
9 | # Build results
10 |
11 | [Dd]ebug/
12 | [Rr]elease/
13 | x64/
14 | build/
15 | [Bb]in/
16 | [Oo]bj/
17 |
18 | # Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets
19 | !packages/*/build/
20 |
21 | # MSTest test Results
22 | [Tt]est[Rr]esult*/
23 | [Bb]uild[Ll]og.*
24 |
25 | *_i.c
26 | *_p.c
27 | *.ilk
28 | *.meta
29 | *.obj
30 | *.pch
31 | *.pdb
32 | *.pgc
33 | *.pgd
34 | *.rsp
35 | *.sbr
36 | *.tlb
37 | *.tli
38 | *.tlh
39 | *.tmp
40 | *.tmp_proj
41 | *.log
42 | *.vspscc
43 | *.vssscc
44 | .builds
45 | *.pidb
46 | *.log
47 | *.scc
48 |
49 | # Visual C++ cache files
50 | ipch/
51 | *.aps
52 | *.ncb
53 | *.opensdf
54 | *.sdf
55 | *.cachefile
56 |
57 | # Visual Studio profiler
58 | *.psess
59 | *.vsp
60 | *.vspx
61 |
62 | # Guidance Automation Toolkit
63 | *.gpState
64 |
65 | # ReSharper is a .NET coding add-in
66 | _ReSharper*/
67 | *.[Rr]e[Ss]harper
68 |
69 | # TeamCity is a build add-in
70 | _TeamCity*
71 |
72 | # DotCover is a Code Coverage Tool
73 | *.dotCover
74 |
75 | # NCrunch
76 | *.ncrunch*
77 | .*crunch*.local.xml
78 |
79 | # Installshield output folder
80 | [Ee]xpress/
81 |
82 | # DocProject is a documentation generator add-in
83 | DocProject/buildhelp/
84 | DocProject/Help/*.HxT
85 | DocProject/Help/*.HxC
86 | DocProject/Help/*.hhc
87 | DocProject/Help/*.hhk
88 | DocProject/Help/*.hhp
89 | DocProject/Help/Html2
90 | DocProject/Help/html
91 |
92 | # Click-Once directory
93 | publish/
94 |
95 | # Publish Web Output
96 | *.Publish.xml
97 |
98 | # NuGet Packages Directory
99 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line
100 | #packages/
101 |
102 | # Windows Azure Build Output
103 | csx
104 | *.build.csdef
105 |
106 | # Windows Store app package directory
107 | AppPackages/
108 |
109 | # Others
110 | sql/
111 | *.Cache
112 | ClientBin/
113 | [Ss]tyle[Cc]op.*
114 | ~$*
115 | *~
116 | *.dbmdl
117 | *.[Pp]ublish.xml
118 | *.pfx
119 | *.publishsettings
120 |
121 | # RIA/Silverlight projects
122 | Generated_Code/
123 |
124 | # Backup & report files from converting an old project file to a newer
125 | # Visual Studio version. Backup files are not needed, because we have git ;-)
126 | _UpgradeReport_Files/
127 | Backup*/
128 | UpgradeLog*.XML
129 | UpgradeLog*.htm
130 |
131 | # SQL Server files
132 | App_Data/*.mdf
133 | App_Data/*.ldf
134 |
135 |
136 | #LightSwitch generated files
137 | GeneratedArtifacts/
138 | _Pvt_Extensions/
139 | ModelManifest.xml
140 |
141 | # =========================
142 | # Windows detritus
143 | # =========================
144 |
145 | # Windows image file caches
146 | Thumbs.db
147 | ehthumbs.db
148 |
149 | # Folder config file
150 | Desktop.ini
151 |
152 | # Recycle Bin used on file shares
153 | $RECYCLE.BIN/
154 |
155 | # Mac desktop service store files
156 | .DS_Store
157 |
--------------------------------------------------------------------------------
/AmazonScrape.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {B855C03D-E2F8-4C93-ADB4-690956D1D567}
8 | WinExe
9 | Properties
10 | AmazonScrape
11 | AmazonScrape
12 | v4.5
13 | 512
14 | {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
15 | 4
16 | false
17 | publish\
18 | true
19 | Disk
20 | false
21 | Foreground
22 | 7
23 | Days
24 | false
25 | false
26 | true
27 | 0
28 | 1.0.0.%2a
29 | false
30 | true
31 |
32 |
33 | AnyCPU
34 | true
35 | full
36 | false
37 | bin\Debug\
38 | DEBUG;TRACE
39 | prompt
40 | 4
41 | false
42 |
43 |
44 | AnyCPU
45 | pdbonly
46 | true
47 | bin\Release\
48 | TRACE
49 | prompt
50 | 4
51 |
52 |
53 | UI\Resources\Images\application_icon.ico
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | 4.0
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | MSBuild:Compile
79 | Designer
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | RangeBoxes.xaml
88 |
89 |
90 | RangeSlider.xaml
91 |
92 |
93 | ScoreDistributionControl.xaml
94 |
95 |
96 | TextBoxPlus.xaml
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | Component
107 |
108 |
109 | Component
110 |
111 |
112 |
113 |
114 |
115 | Designer
116 | MSBuild:Compile
117 |
118 |
119 | MSBuild:Compile
120 | Designer
121 |
122 |
123 | App.xaml
124 | Code
125 |
126 |
127 | MainWindow.xaml
128 | Code
129 |
130 |
131 | Designer
132 | MSBuild:Compile
133 |
134 |
135 | Designer
136 | MSBuild:Compile
137 |
138 |
139 | Designer
140 | MSBuild:Compile
141 |
142 |
143 | Designer
144 | MSBuild:Compile
145 |
146 |
147 |
148 |
149 | Code
150 |
151 |
152 | True
153 | True
154 | Resources.resx
155 |
156 |
157 | True
158 | Settings.settings
159 | True
160 |
161 |
162 | ResXFileCodeGenerator
163 | Resources.Designer.cs
164 |
165 |
166 |
167 | SettingsSingleFileGenerator
168 | Settings.Designer.cs
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 | False
181 | Microsoft .NET Framework 4.5 %28x86 and x64%29
182 | true
183 |
184 |
185 | False
186 | .NET Framework 3.5 SP1 Client Profile
187 | false
188 |
189 |
190 | False
191 | .NET Framework 3.5 SP1
192 | false
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
210 |
--------------------------------------------------------------------------------
/AmazonScrape.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 2012
4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AmazonScrape", "AmazonScrape.csproj", "{B855C03D-E2F8-4C93-ADB4-690956D1D567}"
5 | EndProject
6 | Global
7 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
8 | Debug|Any CPU = Debug|Any CPU
9 | Release|Any CPU = Release|Any CPU
10 | EndGlobalSection
11 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
12 | {B855C03D-E2F8-4C93-ADB4-690956D1D567}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
13 | {B855C03D-E2F8-4C93-ADB4-690956D1D567}.Debug|Any CPU.Build.0 = Debug|Any CPU
14 | {B855C03D-E2F8-4C93-ADB4-690956D1D567}.Release|Any CPU.ActiveCfg = Release|Any CPU
15 | {B855C03D-E2F8-4C93-ADB4-690956D1D567}.Release|Any CPU.Build.0 = Release|Any CPU
16 | {011577DC-F356-4004-B97D-3B0117D48498}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17 | {011577DC-F356-4004-B97D-3B0117D48498}.Debug|Any CPU.Build.0 = Debug|Any CPU
18 | {011577DC-F356-4004-B97D-3B0117D48498}.Release|Any CPU.ActiveCfg = Release|Any CPU
19 | {011577DC-F356-4004-B97D-3B0117D48498}.Release|Any CPU.Build.0 = Release|Any CPU
20 | EndGlobalSection
21 | GlobalSection(SolutionProperties) = preSolution
22 | HideSolutionNode = FALSE
23 | EndGlobalSection
24 | EndGlobal
25 |
--------------------------------------------------------------------------------
/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/App.xaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/App.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 | using System.Windows.Controls;
3 | using System.Windows.Media;
4 |
5 | namespace AmazonScrape
6 | {
7 | ///
8 | /// Interaction logic for App.xaml
9 | ///
10 | public partial class App : Application
11 | {
12 | static App()
13 | {
14 | // Ensure that displayed text uses ClearType globally
15 | TextOptions.TextFormattingModeProperty.OverrideMetadata(typeof(Window),
16 | new FrameworkPropertyMetadata(TextFormattingMode.Display,
17 | FrameworkPropertyMetadataOptions.AffectsMeasure |
18 | FrameworkPropertyMetadataOptions.AffectsRender |
19 | FrameworkPropertyMetadataOptions.Inherits));
20 |
21 | }
22 |
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Constants.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace AmazonScrape
3 | {
4 | public static class Constants
5 | {
6 | // -------------------------------------------------------------------
7 | // Default search parameters
8 | // -------------------------------------------------------------------
9 | public const string DEFAULT_SEARCH_TEXT = @"";
10 | public const string DEFAULT_RESULT_COUNT = @"20";
11 | public const string DEFAULT_MIN_REVIEWS = @"0";
12 | public const bool DEFAULT_MATCH_ALL_TERMS = false;
13 | public const string DEFAULT_LOW_PRICERANGE = @"";
14 | public const string DEFAULT_HIGH_PRICERANGE = @"";
15 | // todo: add review distribution defaults
16 |
17 |
18 |
19 | // -------------------------------------------------------------------
20 | // Major formatting settings
21 | // ( other settings are found in .\UI\Resources\ControlStyles.xaml )
22 | // -------------------------------------------------------------------
23 | public const double DEFAULT_LARGE_TEXT_SIZE = 16.0;
24 | public const double DEFAULT_MEDIUM_TEXT_SIZE = 12.0;
25 | public const double DEFAULT_SMALL_TEXT_SIZE = 10.0;
26 | public const double DEFAULT_BUTTON_FONT_SIZE = 12.0;
27 |
28 |
29 |
30 | // -------------------------------------------------------------------
31 | // URLS and parameters (used by Scraper/Parser)
32 | // -------------------------------------------------------------------
33 | public const string BASE_URL = @"www.amazon.com";
34 | public const string SEARCH_URL =
35 | @"http://www.amazon.com/s/field-keywords=";
36 | public const string SEARCH_URL_PAGE_PARAM = @"&page=";
37 | public const string REVIEW_HISTOGRAM_URL =
38 | @"http://www.amazon.com/gp/customer-reviews/common/du/displayHistoPopAjax.html?&ASIN=";
39 |
40 |
41 |
42 | // -------------------------------------------------------------------
43 | // Program operation settings
44 | // -------------------------------------------------------------------
45 |
46 | // The number of async pageload threads to run (set to 2 or more)
47 | public const int MAX_THREADS = 3;
48 |
49 | // Ensures prime eligibility accuracy by loading an additional
50 | // page for each result (slows application execution significantly)
51 | // Under normal circumstances, this should remain "false".
52 | public const bool USE_STRICT_PRIME_ELIGIBILITY = false;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Model/Logic/ItemValidator.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace AmazonScrape
8 | {
9 | ///
10 | /// Static methods determine whether the inputs meet the user's
11 | /// search criteria
12 | ///
13 | public static class ItemValidator
14 | {
15 | public static bool ValidateItemName(SearchCriteria criteria, string itemName)
16 | {
17 | if (criteria.MatchAllSearchTerms)
18 | {
19 | if (itemName == null) { return false; }
20 |
21 | // Make sure every search term is present in the item name
22 | string[] terms = criteria.SearchText.ToLower().Split(' ');
23 | for (int i = 0; i < terms.Count(); i++)
24 | {
25 | if (!itemName.ToLower().Contains(terms[i]))
26 | {
27 | return false;
28 | }
29 | }
30 | }
31 |
32 | return true;
33 | }
34 |
35 | public static bool ValidateReviewCount(SearchCriteria criteria, int count)
36 | {
37 | if (count < criteria.MinNumberReviews)
38 | {
39 | return false;
40 | }
41 |
42 | return true;
43 | }
44 |
45 | public static bool ValidatePriceRange(SearchCriteria criteria, DoubleRange priceRange)
46 | {
47 | // If the user specified a price range, but the item has no price information, fail
48 | if (criteria.PriceRange.HasRangeSpecified && !priceRange.HasRangeSpecified) { return false; }
49 |
50 | if (criteria.PriceRange != null && criteria.PriceRange.HasRangeSpecified)
51 | {
52 | if (priceRange == null) return false;
53 | if (!criteria.PriceRange.Overlaps(priceRange)) return false;
54 | }
55 |
56 | return true;
57 | }
58 |
59 | public static bool ValidateReviewDistribution(SearchCriteria criteria, ScoreDistribution scoreDistribution)
60 | {
61 |
62 | // Test each of the review percentage criteria
63 | if (scoreDistribution != null)
64 | {
65 | if (!criteria.ScoreDistribution.OneStar.Contains(scoreDistribution.OneStar))
66 | { return false; }
67 |
68 | if (!criteria.ScoreDistribution.TwoStar.Contains(scoreDistribution.TwoStar))
69 | { return false; }
70 |
71 | if (!criteria.ScoreDistribution.ThreeStar.Contains(scoreDistribution.ThreeStar))
72 | { return false; }
73 |
74 | if (!criteria.ScoreDistribution.FourStar.Contains(scoreDistribution.FourStar))
75 | { return false; }
76 |
77 | if (!criteria.ScoreDistribution.FiveStar.Contains(scoreDistribution.FiveStar))
78 | { return false; }
79 | }
80 |
81 | return true;
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/Model/Logic/PageManager.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.ComponentModel;
4 | using System.Diagnostics;
5 | using System.Threading;
6 | using System.Windows.Media.Imaging;
7 |
8 | namespace AmazonScrape
9 | {
10 | ///
11 | /// Manages the workload of one page of search results.
12 | /// Calls appropriate methods to load, parse,
13 | /// validate and return results to the SearchManager.
14 | ///
15 | /// Directly calls ItemValidator, Parser, Scraper
16 | public class PageManager : BackgroundWorker
17 | {
18 |
19 | public int PageNumber { get { return _pageNumber; } }
20 | public int ResultsOnPage { get { return _pageResultCount; } }
21 | public bool IsFirstPage { get { return _pageNumber == 1; } }
22 |
23 | // NoResults means that there were zero parsable results
24 | // Finished means that the results that were found are all returned
25 | // Error indicates a problem loading the page
26 | public enum Status { Working, NoResults, Finished, Error };
27 |
28 | public Status WorkStatus { get { return _status; } }
29 | private Status _status;
30 |
31 | private readonly int _pageNumber; // Which search page index we're on
32 | private int _pageResultCount; // The number of results on this page
33 |
34 | // Holds the html for each individual product returned
35 | private List _productHtmlSegments = new List();
36 |
37 | // Pass in the method you'd like to use to get the html for this page
38 | // (allows easier test injection)
39 | // int: Search page number to load
40 | // string: Search terms to use
41 | //
42 | // Returns: string html of the page load result.
43 | private readonly Func _pageLoadMethod;
44 | private readonly SearchCriteria _searchCriteria;
45 |
46 | // The final list of results to be passed back
47 | private List> _results =
48 | new List>();
49 |
50 | ///
51 | /// Creates and dispatches a PageManager to load a search page,
52 | /// parse, extract, validate and return results using the
53 | /// parameter callback EventHandlers
54 | ///
55 | /// Which search page number to load
56 | /// The user's search criteria
57 | /// Supply the method to retrieve html
58 | /// Callback for progress updates
59 | /// Callback when work complete
60 | public PageManager(int pageNumber,
61 | SearchCriteria criteria,
62 | Func pageLoadMethod, // see explanation above
63 | ProgressChangedEventHandler updateProgressMethod,
64 | RunWorkerCompletedEventHandler workerCompleteMethod)
65 | {
66 |
67 | if (pageNumber < 1)
68 | {
69 | string msg = "Supplied page number ({0}) was < 0!";
70 | msg = string.Format(msg,pageNumber);
71 | throw new ArgumentOutOfRangeException(msg);
72 | }
73 |
74 | if (pageLoadMethod == null)
75 | {
76 | string msg = "Provided a null method to obtain page HTML!";
77 | throw new InvalidOperationException(msg);
78 | }
79 |
80 | ProgressChanged += updateProgressMethod; // Callback
81 | RunWorkerCompleted += workerCompleteMethod; // Callback
82 | DoWork += Work;
83 |
84 | _pageLoadMethod = pageLoadMethod;
85 | _searchCriteria = criteria;
86 | _pageNumber = pageNumber;
87 |
88 | WorkerReportsProgress = true;
89 | WorkerSupportsCancellation = true;
90 |
91 | }
92 |
93 | ///
94 | /// Loads, chops up, parses and validates one page worth of results.
95 | ///
96 | ///
97 | ///
98 | public void Work(object sender, DoWorkEventArgs e)
99 | {
100 | _status = Status.Working;
101 | if (Thread.CurrentThread.Name == null)
102 | Thread.CurrentThread.Name = "Page " + _pageNumber.ToString() + " worker";
103 |
104 | // Set the RunWorkEventArgs so we can check its status on completion
105 | e.Result = this;
106 |
107 | // Will hold the page's html broken up by each individual product
108 | _productHtmlSegments = new List();
109 |
110 | // Gets the entire page's html
111 | string pageHtml = _pageLoadMethod(_pageNumber,
112 | _searchCriteria.SearchText);
113 |
114 | // Get the number of results on this page
115 | _pageResultCount = Parser.GetPageResultCount(pageHtml);
116 |
117 | // If there are no results, set the status accordingly and exit
118 | if (_pageResultCount == 0)
119 | {
120 | _status = Status.NoResults;
121 | return;
122 | }
123 | else // There are results
124 | {
125 | // Break apart the page html by product
126 | // so they can be parsed individually
127 | _productHtmlSegments = Parser.GetPageResultItemHtml(pageHtml,
128 | _pageResultCount);
129 | }
130 |
131 | List> results = new List>();
132 |
133 | // Parse and validate each result, adding to the result list
134 | foreach (string productHtml in _productHtmlSegments)
135 | {
136 | Result result =
137 | ParseAndValidateProductHtml(productHtml);
138 |
139 | // Don't worry about reporting the progress percentage here.
140 | // The SearchManager will look at the total results returned
141 | // and compare with the results requested and report that
142 | // percentage to the UI (passing in a dummy zero here)
143 | ReportProgress(0, result);
144 | }
145 |
146 | // The RunWorkerComplete method fires when method completes
147 | // This is used as a signal to the SearchManager that we
148 | // are clear to spawn another thread if necessary.
149 | _status = Status.Finished;
150 | }
151 |
152 | ///
153 | /// Parses and validates a single product's html, returning a
154 | /// Result containing error messages or the valid AmazonItem
155 | ///
156 | /// Product html to parse
157 | /// List of AmazonItem Results
158 | private Result ParseAndValidateProductHtml(string html)
159 | {
160 | Result result = new Result();
161 |
162 | // Parse each item's html and exit early if validation fails on any item.
163 | string name = Parser.GetProductName(html);
164 | if (name == null || name.Length == 0)
165 | {
166 | // Do not report a "missing product name" status message here.
167 | // Sometimes Amazon injects blurbs or information
168 | // sections in lieu of results (book results, for example).
169 | // This should not trigger an error.
170 | return result;
171 | }
172 |
173 | if (!ItemValidator.ValidateItemName(_searchCriteria, name))
174 | {
175 | result.StatusMessage = name + " doesn't contain all search criteria.";
176 | return result;
177 | }
178 |
179 | // Scrape the review histogram to obtain the review distribution
180 | // and the review count (originally review count was being
181 | // obtained on the main page, but Amazon removes review
182 | // information from search results if they smell a bot).
183 | string reviewHistogramHtml = Parser.GetReviewHistogramHtml(html);
184 | if (reviewHistogramHtml == null || reviewHistogramHtml.Length == 0)
185 | {
186 | string msg = "Couldn't obtain review histogram data";
187 | result.ErrorMessage = msg;
188 | }
189 |
190 | ScoreDistribution scoreDistribution =
191 | Parser.GetScoreDistribution(reviewHistogramHtml);
192 | if (!ItemValidator.ValidateReviewDistribution(_searchCriteria, scoreDistribution))
193 | {
194 | result.StatusMessage = name + " doesn't fall within your review distribution.";
195 | return result;
196 | }
197 |
198 | int reviewCount = Parser.GetReviewCount(reviewHistogramHtml);
199 | if (!ItemValidator.ValidateReviewCount(_searchCriteria, reviewCount))
200 | {
201 | string message = name + " ";
202 |
203 | if (reviewCount == 0) { message += "doesn't have any reviews."; }
204 | else
205 | {
206 | message += "only has " + reviewCount.ToString() + " reviews.";
207 | }
208 | result.StatusMessage = message;
209 | return result;
210 | }
211 |
212 | DoubleRange priceRange = Parser.GetPriceRange(html);
213 | if (!ItemValidator.ValidatePriceRange(_searchCriteria, priceRange))
214 | {
215 | result.StatusMessage = name + " doesn't fit in your price range.";
216 | return result;
217 | }
218 |
219 | // Grab the item's URL so the user can go directly to the product page
220 | Uri url = Parser.GetURL(html);
221 |
222 | // Note: Right now there's no UI capability of validating average rating
223 | double rating = Parser.GetRating(reviewHistogramHtml);
224 |
225 | // TODO: implement a "prime-only" checkbox in the UI
226 | bool primeEligibility;
227 | if (_searchCriteria.StrictPrimeEligibility)
228 | {
229 | primeEligibility = Parser.GetStrictPrimeEligibility(url);
230 | }
231 | else
232 | {
233 | primeEligibility = Parser.GetFuzzyPrimeEligibility(html);
234 | }
235 |
236 | // Leave the image load for last since it takes longer and if the
237 | // item doesn't pass validation we don't waste time downloading
238 | BitmapImage image = Parser.GetImageThumbnail(html);
239 |
240 | // We have everything we need, build the AmazonItem to be returned
241 | result.Value = new AmazonItem(name,
242 | reviewCount,
243 | priceRange,
244 | scoreDistribution,
245 | url,
246 | rating,
247 | primeEligibility,
248 | image);
249 |
250 | return result;
251 | }
252 |
253 | ///
254 | /// Outputs useful information about the PageManager
255 | ///
256 | /// string description
257 | public override string ToString()
258 | {
259 | string msg = base.ToString() + Environment.NewLine;
260 | msg += "----------------------------------" + Environment.NewLine;
261 | msg += "Search Page Number: {0}" + Environment.NewLine;
262 | msg += "Worker Status: {1}" + Environment.NewLine;
263 | msg += "Results on Page: {2}" + Environment.NewLine;
264 |
265 | return string.Format(msg,
266 | _pageNumber,
267 | WorkStatus,
268 | _pageResultCount);
269 |
270 | }
271 | }
272 | }
273 |
--------------------------------------------------------------------------------
/Model/Logic/Parser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Generic;
4 | using System.Diagnostics;
5 | using System.Text.RegularExpressions;
6 | using System.Windows.Media.Imaging;
7 |
8 | namespace AmazonScrape
9 | {
10 | ///
11 | /// Contains static methods to process Amazon html and return product information
12 | ///
13 | public static class Parser
14 | {
15 | ///
16 | /// Finds and returns a list of signed/unsigned integers/doubles
17 | /// parsed from the supplied string. Comma-formatted numbers are
18 | /// recognized.
19 | ///
20 | /// Only recognizes "correctly formatted" comma pattern:
21 | /// e.g. 1,234.123 or 12,345,678.123 but not 1,23,4.123
22 | /// Optional parameter parseCount allows the user to limit the number
23 | /// of numbers returned.
24 | /// Note: limiting the amount of results does NOT improve performance;
25 | /// it simply returns the firs N results found.
26 | /// The string to parse
27 | /// The number of double values
28 | /// it will attempt to parse
29 | /// List of Double values
30 | public static List ParseDoubleValues(string text,
31 | int parseCount = -1)
32 | {
33 | // Full pattern:
34 | // (((-?)(\d{1,3}(,\d{3})+)|(-?)(\d)+)(\.(\d)*)?)|((-)?\.(\d)+)
35 |
36 | List results = new List();
37 | if (text == null) { return results; }
38 |
39 | // Optional negative sign and one or more digits
40 | // Valid: "1234", "-1234", "0", "-0"
41 | string signedIntegerNoCommas = @"(-?)(\d)+";
42 |
43 | // Optional negative sign and digits grouped by commas
44 | // Valid: "1,234", "-1,234", "1,234,567"
45 | // INVALID: "12,34" <-- does not fit "normal" comma pattern
46 | string signedIntegerCommas = @"(-?)(\d{1,3}(,\d{3})+)";
47 |
48 | string or = @"|";
49 |
50 | // Optional decimal point and digits
51 | // Valid: ".123", ".0", "", ".12345", "."
52 | string optionalUnsignedDecimalAndTrailingNumbers = @"(\.(\d)*)?";
53 |
54 | // Optional negative sign, decimal point and at least one digit
55 | // Valid: "-.12", ".123"
56 | // INVALID: "", ".", "-."
57 | string requiredSignedDecimalAndTrailingNumbers = @"((-)?\.(\d)+)";
58 |
59 | string pattern = @"";
60 |
61 | // Allow a signed integer with or without commas
62 | // and an optional decimal portion
63 | pattern += @"(" + signedIntegerCommas + or + signedIntegerNoCommas
64 | + @")" + optionalUnsignedDecimalAndTrailingNumbers;
65 |
66 | // OR allow just a decimal portion (with or without sign)
67 | pattern = @"(" + pattern + @")" + or
68 | + requiredSignedDecimalAndTrailingNumbers;
69 |
70 | List matches = GetMultipleRegExMatches(text, pattern);
71 |
72 | int matchIndex = 0;
73 | foreach (string match in matches)
74 | {
75 | // If the user supplied a max number of
76 | // doubles to parse, check to make sure we don't exceed it
77 | if (parseCount > 0)
78 | {
79 | if (matchIndex + 1 > parseCount) break;
80 | }
81 |
82 | try
83 | {
84 | // Get rid of any commas before converting
85 | results.Add(Convert.ToDouble(match.Replace(",", "")));
86 | }
87 | catch
88 | {
89 | string msg = "Unable to convert {0} to a double";
90 | Debug.WriteLine(string.Format(msg, match));
91 | }
92 | matchIndex += 1;
93 | }
94 |
95 | return results;
96 | }
97 |
98 | ///
99 | /// Returns the number of reviews for the product, given the
100 | /// review histogram html (not the product html)
101 | ///
102 | /// html for the review histogram
103 | /// integer number of product reviews
104 | public static int GetReviewCount(string reviewHistogramHtml)
105 | {
106 | string reviewCountPattern = @"(?<=See all ).*?(?= reviews)";
107 |
108 | string reviewCountMatch = GetSingleRegExMatch(reviewHistogramHtml,
109 | reviewCountPattern);
110 |
111 | if (reviewCountPattern.Length == 0) return 0;
112 | reviewCountMatch = reviewCountMatch.Replace(",", "");
113 | int reviewCount = 0;
114 |
115 | try
116 | {
117 | reviewCount = Convert.ToInt32(reviewCountMatch);
118 | }
119 | catch
120 | {
121 | string msg = "Unable to cast review count to an integer ";
122 | msg += "(probably because there were no reviews)";
123 | Debug.WriteLine(msg);
124 | }
125 |
126 | return reviewCount;
127 | }
128 |
129 | ///
130 | /// Given the html of an Amazon search page result, returns
131 | /// the number of product results.
132 | ///
133 | /// html of entire search page
134 | public static int GetPageResultCount(string pageHtml)
135 | {
136 |
137 | // Three possible formats for figuring out the
138 | // number of results on the page:
139 | // -------------------------------------------------
140 | // Case 1: "Showing X Results" (one page)
141 | // Case 2: "Showing X - Y of Z Results" ( >1 page)
142 | // Case 3: "Your search "" did
143 | // not match any products."
144 |
145 | // Grab the section after the resultCount id attribute
146 | // until the next id attribute
147 | string resultCountPattern = @"(?<=id=""resultCount"").*?(?= id=)";
148 | string match = GetSingleRegExMatch(pageHtml, resultCountPattern);
149 |
150 | int resultTotal = 0;
151 |
152 | if (match.Length == 0) return resultTotal;
153 |
154 | // Parse out the numeric values,
155 | // limiting to two maximum (as in Case 2 above)
156 | List resultRange = ParseDoubleValues(match, 2);
157 |
158 | switch (resultRange.Count)
159 | {
160 | case 1:
161 | try
162 | { resultTotal = Convert.ToInt32(resultRange[0]); }
163 | catch { }
164 | break;
165 | case 2:
166 | try
167 | {
168 | // ParseDoubleValues thinks the hyphen in the results
169 | // denotes a negative number.
170 | // e.g. "17-32 of 65,130" will return 17, -32
171 | // Get the absolute values before subtracting.
172 | resultTotal = Convert.ToInt32(
173 | Math.Abs(resultRange[1]) -
174 | (Math.Abs(resultRange[0]) - 1));
175 | }
176 | catch { }
177 | break;
178 | }
179 |
180 | // (Case 3 doesn't need to be handled, since resultTotal
181 | // will fall through and correctly remain 0)
182 |
183 | return resultTotal;
184 | }
185 |
186 | ///
187 | /// Returns a list of individual html product results from an html page
188 | ///
189 | /// The string containing a single page of Amazon search results
190 | /// Specity the number of results the method will attempt to parse
191 | /// A list of strings representing individual html item results
192 | public static List GetPageResultItemHtml(string pageHtml, int resultCount)
193 | {
194 | // NOTE:
195 | // Amazon injects additional search results (commented out in
196 | // javascript) at the bottom of each search page to cache results.
197 | // The parameter _resultCount is obtained from the top of the page
198 | // so that only the results that are visible to the user are
199 | // returned. This was mainly done to fix a bug where duplicate
200 | // records were being returned, but it's probably good practice
201 | // to only consider "visible" results in case Amazon changes its
202 | // caching strategy.
203 |
204 | List results = new List();
205 | TimeSpan timeOut = new TimeSpan(0, 0, 10);
206 |
207 | // Grab the text between each of the results
208 | string resultPattern = @"(?<=result_[0-9]?[0-9]).*?(?=result_[0-9]?[0-9])";
209 | List matches = GetMultipleRegExMatches(pageHtml, resultPattern);
210 |
211 | if (matches.Count < resultCount) { return results; }
212 |
213 | for (int i = 0; i < resultCount ; i++)
214 | {
215 | results.Add(matches[i]);
216 | }
217 |
218 | return results;
219 | }
220 |
221 | ///
222 | /// Extracts the product's name from a single product's html
223 | ///
224 | /// Single product result html
225 | /// Name of product
226 | public static string GetProductName(string itemHtml)
227 | {
228 | string productNamePattern = @"(?<= title="").*?(?="" )";
229 | string match = GetSingleRegExMatch(itemHtml, productNamePattern);
230 |
231 | if (match.Length == 0)
232 | { return null; }
233 |
234 | string productName = Scraper.DecodeHTML(match);
235 |
236 | return productName;
237 | }
238 |
239 | ///
240 | /// Parses a DoubleRange object representing the "high" and "low"
241 | /// prices from the item's html.
242 | ///
243 | /// If there is only one price supplied, set the range to that single value.
244 | /// Single product result html
245 | /// DoubleRange representing the product's pricerange
246 | public static DoubleRange GetPriceRange(string itemHtml)
247 | {
248 | // Dollarsign and Digits grouped by commas plus decimal
249 | // and change (change is required)
250 | string dollarCurrencyFormat = @"\$(\d{1,3}(,\d{3})*).(\d{2})";
251 |
252 | // Optional spaces and hyphen
253 | string spacesAndHyphen = @"\s+-\s+";
254 |
255 | // Grab the end of the preceeding tag, the dollar amount, and
256 | // optionally a hyphen and a high range amount before the
257 | // beginning bracket of the next tag
258 | string pricePattern = ">" + dollarCurrencyFormat + "(" + spacesAndHyphen + dollarCurrencyFormat + ")?" + "<";
259 |
260 | string match = GetSingleRegExMatch(itemHtml, pricePattern);
261 |
262 | // Need to remove the tag beginning and end:
263 | match = match.Trim(new char[] { '<', '>' });
264 |
265 | if (match.Length == 0)
266 | { return new DoubleRange(); }
267 |
268 | List prices = ParseDoubleValues(match, 2);
269 | DoubleRange priceRange = new DoubleRange();
270 | if (prices.Count > 0)
271 | {
272 | priceRange.Low = prices[0];
273 | }
274 |
275 | if (prices.Count > 1)
276 | {
277 | priceRange.High = prices[1];
278 | }
279 |
280 | if (!priceRange.HasHigh)
281 | {
282 | priceRange.High = priceRange.Low;
283 | }
284 |
285 | return priceRange;
286 | }
287 |
288 | ///
289 | /// Given a specific product result html, provides the review histogram
290 | /// html. Used for obtaining review count and review distribution.
291 | ///
292 | ///
293 | /// string html of review histogram
294 | public static string GetReviewHistogramHtml(string itemHtml)
295 | {
296 | // To get the review information without loading an entire new page,
297 | // we will call the review histogram popup URL instead of the main URL
298 | // We need the ASIN of the product to make the call, which is in the same
299 | // DIV tag as the product result #:
300 | string productASINPattern = @"(?<=name="").*?(?="">)";
301 |
302 | string match = GetSingleRegExMatch(itemHtml, productASINPattern);
303 |
304 | // Occassionally Amazon adds attributes to the end of the tag, so
305 | // find the end of attribute containing the ASIN (should be the first
306 | // double quote we encounter).
307 | int endAttributeIndex = match.IndexOf('"');
308 | if (endAttributeIndex > 0)
309 | {
310 | // Truncate anything after
311 | match = match.Substring(0, endAttributeIndex);
312 | }
313 |
314 | if (match.Length == 0)
315 | { return null; }
316 |
317 | // With the product ASIN, make the httprequest to get the review popup data
318 | return Scraper.LoadReviewHistogram(match);
319 | }
320 |
321 | ///
322 | /// Returns a product's review distribution (percentage of reviews in each category)
323 | ///
324 | /// Avoids a full page load by extrating the data from an Ajax popup.
325 | /// Review histogram html
326 | /// ScoreDistribution of reviews
327 | public static ScoreDistribution
328 | GetScoreDistribution(string reviewHistogramHtml)
329 | {
330 | // Find each instance of review percentage. This regex includes more than we need, but we
331 | // wind up only grabbing the first five results, which are the ones we care about.
332 | string reviewDistributionPatterh = @"(?<=title="").*?(?=%)";
333 |
334 | List matches = GetMultipleRegExMatches(reviewHistogramHtml,
335 | reviewDistributionPatterh);
336 |
337 | //MatchCollection reviewScoreMatches = Regex.Matches(reviewHistogramHtml,
338 | // reviewDistributionPatterh, RegexOptions.Singleline);
339 |
340 | // If we can't find any more results, exit
341 | if (matches.Count == 0)
342 | { return null; }
343 |
344 | double[] reviews = new double[5];
345 |
346 | // Feed them into the array backwards so that
347 | // one star reviews are in the zero index
348 | for (int i = 0; i <5; i++)
349 | {
350 | // Reverse the order of the reviews so that index 0 is 1-star,
351 | // index 1 is 2-star, etc.
352 | try
353 | {
354 | // The percentage is at the very end of each string
355 | // Work backwards to build the value
356 | var stack = new Stack();
357 |
358 | for (var strIndex = matches[i].Length - 1; strIndex >= 0; strIndex--)
359 | {
360 | if (!char.IsNumber(matches[i][strIndex]))
361 | {
362 | break;
363 | }
364 | stack.Push(matches[i][strIndex]);
365 | }
366 |
367 | matches[i] = new string(stack.ToArray());
368 |
369 | reviews[4 - i] = Convert.ToDouble(matches[i]);
370 | }
371 | catch (InvalidCastException)
372 | {
373 | string msg = "Unable to cast review score match {0}";
374 | Debug.WriteLine(string.Format(msg,i));
375 | reviews[i] = -1;
376 | }
377 | }
378 |
379 | return new ScoreDistribution(reviews);
380 | }
381 |
382 | ///
383 | /// Attempts to match the supplied pattern to the input
384 | /// string. Obtains multiple matches and returns a
385 | /// list of string matches if successful and an empty
386 | /// list of strings if no matches found.
387 | ///
388 | /// String to search
389 | /// RegEx pattern to search for
390 | /// List of matches or empty list if no matches
391 | private static List GetMultipleRegExMatches(
392 | string inputString,
393 | string regExPattern)
394 | {
395 | string msg;
396 | List results = new List();
397 | try
398 | {
399 | MatchCollection matches = Regex.Matches(inputString,
400 | regExPattern,
401 | RegexOptions.Singleline);
402 | if (matches.Count == 0) return results;
403 |
404 | IEnumerator e = matches.GetEnumerator();
405 | while (e.MoveNext())
406 | {
407 | results.Add(((Match)e.Current).Value);
408 | }
409 | }
410 | catch (ArgumentException ex)
411 | {
412 | msg = regExPattern;
413 | Debug.WriteLine(ex.InnerException +
414 | " argument exception for pattern " + msg);
415 | }
416 | catch (RegexMatchTimeoutException ex)
417 | {
418 | msg = regExPattern;
419 | Debug.WriteLine(ex.InnerException +
420 | " timeout exception for pattern " + msg);
421 | }
422 | return results;
423 |
424 | }
425 |
426 | ///
427 | /// Attempts to match the supplied pattern to the input
428 | /// string. Only obtains a single match and returns the
429 | /// matching string if successful and an empty string if not.
430 | ///
431 | /// String to be searched
432 | /// Pattern to be matched
433 | /// String match or empty string if match not found
434 | private static string GetSingleRegExMatch(string inputString,
435 | string regExPattern)
436 | {
437 | string msg;
438 | string result = "";
439 | try
440 | {
441 | Match match = Regex.Match(inputString,
442 | regExPattern,
443 | RegexOptions.Singleline);
444 | if (match.Success)
445 | {
446 | result = match.Value;
447 | }
448 | }
449 | catch (ArgumentException ex)
450 | {
451 | msg = regExPattern;
452 | Debug.WriteLine(ex.InnerException +
453 | " argument exception for pattern " + msg);
454 | }
455 | catch (RegexMatchTimeoutException ex)
456 | {
457 | msg = regExPattern;
458 | Debug.WriteLine(ex.InnerException +
459 | " timeout exception for pattern " + msg);
460 | }
461 | return result;
462 | }
463 |
464 | ///
465 | /// Returns a product's average review rating (double)
466 | ///
467 | /// html of the review histogram
468 | /// -1 if no rating, otherwise double rating value
469 | public static double GetRating(string reviewHistogramHtml)
470 | {
471 | string ratingPattern = @"[0-5].?[0-9]? out of 5 stars";
472 |
473 | string rating = GetSingleRegExMatch(reviewHistogramHtml,
474 | ratingPattern);
475 |
476 | double result = -1;
477 |
478 | if (rating.Length == 0) return result;
479 |
480 | try
481 | {
482 | // Two possible formats:
483 | // 1) Decimal value included, e.g. 4.5
484 | if (rating.Contains("."))
485 | {
486 |
487 | result = Convert.ToDouble(rating.Substring(0, 3));
488 | }
489 | else // 2) No decimal, e.g. 4
490 | {
491 | result = Convert.ToDouble(rating.Substring(0, 1));
492 | }
493 |
494 | }
495 | catch (InvalidCastException)
496 | {
497 | Debug.WriteLine("Unable to cast product rating to a double.");
498 | }
499 |
500 | return result;
501 | }
502 |
503 | ///
504 | /// Using an item's html, determines Prime eligibility with passable accuracy.
505 | ///
506 | /// Bases Prime eligibility on "Super Saver Shipping", which is accurate most of the time.
507 | /// Done this way to avoid an extra page load. Notable speed increase.
508 | ///
509 | ///
510 | public static bool GetFuzzyPrimeEligibility(string itemHtml)
511 | {
512 | string fuzzyPrimeEligibilityPattern = @".*?FREE.*?Shipping";
513 |
514 | string match = GetSingleRegExMatch(itemHtml, fuzzyPrimeEligibilityPattern);
515 |
516 | return (match.Length > 0);
517 | }
518 |
519 | ///
520 | /// Uses an additional page load to determine Prime eligibility
521 | /// with accuracy
522 | ///
523 | ///
524 | ///
525 | public static bool GetStrictPrimeEligibility(Uri productURL)
526 | {
527 | string html = Scraper.CreateHttpRequest(productURL);
528 |
529 | // Non-prime eligible results call this function with a "0" first
530 | // parameter; here we look specifically for "1", which
531 | // denotes prime eligibility
532 | string primeEligiblePattern = @"bbopJS.initialize\(1,";
533 |
534 | string match = GetSingleRegExMatch(html, primeEligiblePattern);
535 |
536 | return (match.Length > 0);
537 | }
538 |
539 | ///
540 | /// Returns product ASIN - unique product identifier, used in URLs
541 | ///
542 | ///
543 | /// String ASIN value
544 | public static string GetProductAsin(string itemHtml)
545 | {
546 | string productAsinPattern = @"(?<=data-asin="").*?(?="" )";
547 | return GetSingleRegExMatch(itemHtml, productAsinPattern);
548 | }
549 |
550 | ///
551 | /// Extracts a product's Amazon URL.
552 | ///
553 | /// Used when user clicks to access the product's Amazon listing.
554 | ///
555 | ///
556 | public static Uri GetURL(string itemHtml)
557 | {
558 | string asin = GetProductAsin(itemHtml);
559 | string url = "https://" + Constants.BASE_URL + "/dp/" + asin;
560 |
561 | if (Uri.IsWellFormedUriString(url, UriKind.Absolute))
562 | {
563 | return new Uri(url);
564 | }
565 | else
566 | {
567 | return null;
568 | }
569 | }
570 |
571 | ///
572 | /// Parses out the URL to the product's image thumbnail (if one exists)
573 | // and then calls DownloadWebImage to return a BitmapImage
574 | ///
575 | ///
576 | ///
577 | public static BitmapImage GetImageThumbnail(string itemHtml)
578 | {
579 | // TODO: does Amazon use a standardized image format?
580 | // For now, allowing for multiple possible formats.
581 | string imageURLPattern = @"(http(s?):/)(/[^/]+)+\.(?:jpg|gif|png)";
582 |
583 | string match = GetSingleRegExMatch(itemHtml, imageURLPattern);
584 |
585 | if (match.Length == 0)
586 | { return null; }
587 |
588 | if (Uri.IsWellFormedUriString(match, UriKind.Absolute))
589 | {
590 | Uri imageURL = new Uri(match);
591 | return Scraper.DownloadWebImage(imageURL);
592 | }
593 | else
594 | {
595 | return null;
596 | }
597 | }
598 | }
599 | }
600 |
--------------------------------------------------------------------------------
/Model/Logic/Scraper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using System.Net;
7 | using System.Text.RegularExpressions;
8 | using System.IO;
9 | using System.Windows;
10 | using System.Xml.Linq;
11 | using System.Windows.Media.Imaging;
12 | using System.Windows.Threading;
13 | using System.Threading;
14 | using System.Windows.Controls;
15 | using System.Web;
16 |
17 | namespace AmazonScrape
18 | {
19 | ///
20 | /// Performs page and image loads and provides methods to
21 | /// encode/decode strings.
22 | ///
23 | public static class Scraper
24 | {
25 | ///
26 | /// Encodes the supplied string for use as a URL
27 | ///
28 | /// string to encode
29 | /// URL-encoded string
30 | public static string EncodeURL(string URL)
31 | {
32 | try
33 | {
34 | return HttpUtility.UrlEncode(URL).Replace("+","%20");
35 | }
36 | catch
37 | {
38 | string msg = "Unable to encode URL: " + URL;
39 | throw new ArgumentException(msg);
40 | }
41 |
42 | }
43 |
44 | ///
45 | /// Decodes the HTML-encoded sections of the supplied string
46 | ///
47 | /// HTML-encoded string
48 | /// decoded string
49 | public static string DecodeHTML(string html)
50 | {
51 | string result = "";
52 | try
53 | {
54 | result = HttpUtility.HtmlDecode(html);
55 | }
56 | catch
57 | {
58 | string msg = "Unable to decode HTML: " + html;
59 | throw new ArgumentException(msg);
60 | }
61 |
62 | return result;
63 | }
64 |
65 | ///
66 | /// Given a product's unique Amazon ID, loads the review distribution histogram.
67 | /// Much faster than an entire pageload for detailed review info.
68 | ///
69 | ///
70 | ///
71 | public static string LoadReviewHistogram(string asin)
72 | {
73 | Uri reviewHistogramPopupURL = new Uri(Constants.REVIEW_HISTOGRAM_URL + asin);
74 |
75 | return Scraper.CreateHttpRequest(reviewHistogramPopupURL);
76 | }
77 |
78 | ///
79 | /// Loads the result page based on the criteria and the supplied page index, determines the number of valid
80 | /// results on the page, and returns a list of strings representing the markup for each result on the page.
81 | /// Those results can be used by the other Scraper methods to obtain specific pieces of data (e.g. product name).
82 | ///
83 | ///
84 | ///
85 | ///
86 | public static string LoadSearchPage(int pageIndex, string searchTerms)
87 | {
88 | if (searchTerms == null) return "";
89 |
90 | // Encode characters that are not URL-friendly
91 | // example: "C#" should become "C%23"
92 | searchTerms = EncodeURL(searchTerms);
93 |
94 | string URL = Constants.SEARCH_URL + searchTerms + Constants.SEARCH_URL_PAGE_PARAM + pageIndex.ToString();
95 |
96 | return CreateHttpRequest(new Uri(URL));
97 | }
98 |
99 | public static BitmapImage DownloadWebImage(Uri url)
100 | {
101 | // TODO: this method needs to gracefully handle unresolvable URLs and connection time-outs
102 | BitmapImage bitmap = new BitmapImage(url,
103 | new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.CacheIfAvailable));
104 |
105 | // To make the BitmapImages thread-safe for the BackgroundWorker, they need to
106 | // be frozen (bitmap.Freeze()), but they shouldn't be frozen until they're done downloading.
107 | // We have to force the UI thread to wait until the image is downloaded so we can freeze it.
108 | // Otherwise, we get images in the grid that are partially-downloaded.
109 |
110 | // TODO: this is poor design, but after much searching, there may not be a better solution.
111 | // according to MSDN, DispatcherFrames can be implemented for
112 | // "Short running, very specific frames that exit when an important criteria is met."
113 | while (bitmap.IsDownloading) { DoEvents(); };
114 | bitmap.Freeze(); // Bitmap is now thread-safe and can be operated on by the backgroundworker
115 | return bitmap;
116 | }
117 |
118 | ///
119 | /// Used to force the background worker to wait for a condition before proceeding.
120 | ///
121 | public static void DoEvents()
122 | {
123 | try
124 | {
125 | DispatcherFrame frame = new DispatcherFrame();
126 | Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
127 | new DispatcherOperationCallback((f) =>
128 | {
129 | ((DispatcherFrame)f).Continue = false;
130 | return null;
131 | }), frame);
132 | Dispatcher.PushFrame(frame);
133 | }
134 | catch (OutOfMemoryException)
135 | {
136 | // TODO: If this Exception is ever hit, we want to handle it by
137 | // cancelling the search. See if there is a clean way of doing that.
138 | MessageBox.Show("Out of memory");
139 | }
140 | }
141 |
142 | ///
143 | /// Given a URL, loads and returns a string representing a web page's markup
144 | ///
145 | ///
146 | ///
147 | public static String CreateHttpRequest(Uri URL)
148 | {
149 | WebRequest request = HttpWebRequest.Create(URL);
150 | request.Method = "GET";
151 |
152 | String html = "";
153 | try
154 | {
155 | using (StreamReader reader = new StreamReader(request.GetResponse().GetResponseStream()))
156 | {
157 | html = reader.ReadToEnd();
158 | }
159 | }
160 | catch (Exception)
161 | {
162 | MessageBox.Show("Unable to load page " + URL.ToString() + ". Check your internet connection.");
163 | }
164 | return html;
165 | }
166 |
167 | } // Class
168 | } // Namespace
--------------------------------------------------------------------------------
/Model/Logic/SearchManager.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ComponentModel;
3 | using System.Diagnostics;
4 | using System.Linq;
5 | using System.Threading;
6 |
7 | namespace AmazonScrape
8 | {
9 | ///
10 | /// Manages the logical workflow of the application.
11 | /// Creates and deploys PageManagers.
12 | /// Returns WorkProgress object to report status and successful results.
13 | ///
14 | public sealed class SearchManager: BackgroundWorker
15 | {
16 | // Total number of results considered in the search (used for testing)
17 | private int _resultCount = 0;
18 |
19 | // The number of results matching the user's criteria.
20 | // Used when calculating percent complete.
21 | private int _validResultCount = 0;
22 |
23 | private SearchCriteria _searchCriteria; // User-specified search criteria
24 | private bool _working; // Is the main search thread still active?
25 | private readonly int _threadCount; // Number of PageManagers to spawn
26 |
27 | // Loads/parses/validates individual pages asyncrhonously
28 | // The "worker threads" of the application
29 | private PageManager[] _pageManagers;
30 |
31 | // Maintain the highest search page currently being scraped
32 | private int _pageNumber = 1;
33 |
34 | ///
35 | /// Create and start work on a new search. Spawns PageManagers.
36 | ///
37 | /// User-supplied search criteria
38 | /// Number of pages to search asynchronously
39 | public SearchManager(SearchCriteria searchCriteria, int threadCount)
40 | {
41 | if (threadCount < 2)
42 | {
43 | string msg = "Application misconfigured. ";
44 | msg += "Set the MAX_THREADS constant to a value > 1";
45 | throw new ArgumentException(msg);
46 | }
47 |
48 | _searchCriteria = searchCriteria;
49 | _threadCount = threadCount;
50 | DoWork += Work;
51 |
52 | WorkerReportsProgress = true;
53 | WorkerSupportsCancellation = true;
54 | }
55 |
56 | ///
57 | /// Sets up PageManager threads and begin search
58 | ///
59 | ///
60 | ///
61 | private void Work(object sender, DoWorkEventArgs e)
62 | {
63 | _working = true;
64 |
65 | if (Thread.CurrentThread.Name == null)
66 | Thread.CurrentThread.Name = "Search Manager: " +
67 | _searchCriteria.SearchText;
68 |
69 | System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
70 | stopwatch.Start();
71 |
72 | // Set up the page managers, each running on an async thread
73 | _pageManagers = new PageManager[_threadCount];
74 |
75 | for (int i = 0; i < _pageManagers.Count(); i++)
76 | {
77 | // Keep track of the highest page we've
78 | // attempted to scrape (page numbers are not zero-based)
79 | _pageNumber = i+1;
80 |
81 | // PageManagers internally fire off an async worker which each
82 | // call the ResultReturned / WorkerFinished event handlers.
83 | _pageManagers[i] = new PageManager(
84 | _pageNumber,
85 | _searchCriteria,
86 | Scraper.LoadSearchPage, // inject method for testing here
87 | ResultReturned,
88 | WorkerFinished);
89 | _pageManagers[i].RunWorkerAsync();
90 | }
91 |
92 | while (_working)
93 | {
94 | // User can cancel a search through the UI.
95 | if (CancellationPending)
96 | {
97 | HaltAllOngoingWork();
98 | }
99 | }
100 |
101 | stopwatch.Stop();
102 | string msg = "Search time : {0} ms" + Environment.NewLine;
103 | msg = string.Format(msg, stopwatch.ElapsedMilliseconds);
104 | Debug.WriteLine(msg);
105 | }
106 |
107 | ///
108 | /// True if the search was cancelled, we've reached 100% of the
109 | /// desired number of results, or all of the threads are finished
110 | ///
111 | ///
112 | private bool IsWorkFinished()
113 | {
114 | // If the main search has been cancelled by the user
115 | if (_working == false)
116 | {
117 | string msg = "SearchManager no longer set to 'Working'";
118 | Debug.WriteLine(msg);
119 | return true;
120 | }
121 |
122 | // If all specified results have been returned
123 | if (GetPercentComplete() >= 100)
124 | {
125 | Debug.WriteLine("=== Percent Complete at or above 100 ===");
126 | return true;
127 | }
128 |
129 | // If all worker threads are no longer working
130 | // (finished / no more results to search)
131 | int workingCount = _pageManagers.Where(i => i.WorkStatus ==
132 | PageManager.Status.Working).Count();
133 | if (workingCount == 0)
134 | {
135 | Debug.WriteLine("No working PageManagers (none with Working status)");
136 | OutputPageManagerStatus();
137 | return true;
138 | }
139 |
140 | return false;
141 | }
142 |
143 | ///
144 | /// Called by the PageManagers whenever they have a result. Returns
145 | /// the result back to the UI. Also checks and stops work if finished.
146 | ///
147 | /// sender object
148 | /// Result
149 | public void ResultReturned(object obj, ProgressChangedEventArgs args)
150 | {
151 |
152 | string msg = "Number of results considered:{0}";
153 | msg = string.Format(msg, _resultCount);
154 | _resultCount += 1;
155 | Debug.WriteLine(msg);
156 |
157 | Result result;
158 |
159 | // Grab the result
160 | if (args.UserState.GetType() == typeof(Result))
161 | {
162 | result = (Result)args.UserState;
163 |
164 | // If we're already done, stop all threads
165 | // still active and exit
166 | if (IsWorkFinished())
167 | {
168 | HaltAllOngoingWork();
169 | return;
170 | }
171 |
172 | ReportProgress(GetPercentComplete(), result);
173 |
174 | // If it was a result that fit our search critera, update
175 | // our counter (used by the GetPercentComplete method)
176 | if (result.HasReturnValue) _validResultCount += 1;
177 |
178 | }
179 | }
180 |
181 | ///
182 | /// Stops and disposes all PageManager threads and marks
183 | /// the main search as finished working
184 | ///
185 | private void HaltAllOngoingWork()
186 | {
187 | // End each worker thread
188 | foreach (PageManager page in _pageManagers)
189 | {
190 | page.CancelAsync();
191 | page.Dispose();
192 | }
193 |
194 | _working = false;
195 |
196 | }
197 |
198 | ///
199 | /// Called by PageManagers when their task is complete.
200 | /// If there is still work to be done, replaces the finished
201 | /// pageManager with a new one (creates a new thread)
202 | /// Exits if all work is complete.
203 | ///
204 | /// sender object
205 | /// Result
206 | public void WorkerFinished(object obj, RunWorkerCompletedEventArgs args)
207 | {
208 | // See if any of the workers are reporting that they're out
209 | // of results.
210 | bool outOfResults = _pageManagers.Any(i => i.WorkStatus ==
211 | PageManager.Status.NoResults);
212 |
213 | // If so, don't deploy another thread.
214 | if (outOfResults)
215 | {
216 | string msg = "PageManager reporting no more results;";
217 | msg += " not deploying another thread.";
218 | Debug.WriteLine(msg);
219 | return;
220 | }
221 |
222 | // Or if there are no threads that are marked "Working",
223 | // we are done
224 | if (IsWorkFinished())
225 | {
226 | HaltAllOngoingWork();
227 | return;
228 | }
229 |
230 | if (args == null) return;
231 | if (args.Error != null)
232 | {
233 | Debug.WriteLine(args.Error.Message);
234 | return;
235 | }
236 |
237 | if (args.Result == null ||
238 | args.Result.GetType() != typeof(PageManager)) return;
239 |
240 | // If this PageManager is done but we haven't hit our
241 | // target number of results, we should spawn a new thread
242 | PageManager finished = (PageManager)args.Result;
243 |
244 | // Get the index of the PageManager whose search page number
245 | // matches the one that just finished (we're going to replace
246 | // it with a new PageManager)
247 | int index =_pageManagers.ToList().FindIndex(i =>
248 | i.PageNumber == finished.PageNumber);
249 |
250 | // Increment the variable that tracks the
251 | // highest page number we've searched so far
252 | // TODO: since page number is shared state, there is a
253 | // slight chance that two PageManagers might hit this
254 | // code
255 | _pageNumber += 1;
256 |
257 | // Start searching a new page
258 | PageManager newPageManager = new PageManager(
259 | _pageNumber,
260 | _searchCriteria,
261 | Scraper.LoadSearchPage, // inject method for testing here
262 | ResultReturned,
263 | WorkerFinished);
264 |
265 |
266 | _pageManagers[index].Dispose(); // get rid of old one
267 | _pageManagers[index] = newPageManager;
268 | _pageManagers[index].RunWorkerAsync();
269 |
270 | }
271 |
272 | ///
273 | /// Returns integer percentage of overall progress, based on the number
274 | /// of retrieved and validated results.
275 | ///
276 | ///
277 | public int GetPercentComplete()
278 | {
279 | // NumberOfResults is validated in the UI to be > 0
280 | // Catch DBZ error anyway (would result in end of search if error)
281 | int percent = 100;
282 | try
283 | {
284 | decimal resultsCount = (decimal)_validResultCount;
285 | decimal totalResults = (decimal)_searchCriteria.NumberOfResults;
286 | percent = (int)((resultsCount / totalResults)*100);
287 | } catch
288 | { }
289 |
290 | return percent;
291 | }
292 |
293 | ///
294 | /// Write PageManager WorkStatus to console for debugging
295 | ///
296 | private void OutputPageManagerStatus()
297 | {
298 | string msg = "";
299 | for (int i = 0; i < _pageManagers.Count(); i++)
300 | {
301 | msg = "Thread index : {0}" + Environment.NewLine;
302 | msg += "{1}" + Environment.NewLine + Environment.NewLine;
303 | msg = string.Format(msg, i, _pageManagers[i]);
304 | Debug.WriteLine(msg);
305 | }
306 | }
307 |
308 | }
309 | }
310 |
--------------------------------------------------------------------------------
/Model/Structure/AmazonItem.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using System.Windows.Media;
7 | using System.Windows.Media.Imaging;
8 |
9 | namespace AmazonScrape
10 | {
11 | ///
12 | /// Represents an Amazon product
13 | /// Used to populate the data grid.
14 | ///
15 | public class AmazonItem
16 | {
17 | // Data grid doesn't display null values, so using
18 | // all nullable public fields
19 | public String Name { get { return _name; } }
20 | public ImageSource ProductImage { get { return _image; } set { ProductImage = value; } }
21 | public Uri URL { get { return _url; } }
22 |
23 | public Double? LowPrice
24 | {
25 | get
26 | {
27 | if (_priceRange.Low == double.MinValue) { return null; }
28 | else { return _priceRange.Low; }
29 | }
30 | }
31 |
32 | public Double? HighPrice
33 | {
34 | get
35 | {
36 | if (_priceRange.High == double.MaxValue || _priceRange.High == _priceRange.Low ) { return null; }
37 | else { return _priceRange.High; }
38 | }
39 | }
40 |
41 | public Double? Rating
42 | {
43 | get
44 | {
45 | if (_rating == -1) return null;
46 | else return _rating;
47 | }
48 | }
49 | public Int32? ReviewCount { get { return _reviewCount; } }
50 | public ImageSource PrimeLogoImage
51 | {
52 | get
53 | {
54 | if (_primeEligible) return ResourceLoader.GetPrimeLogoBitmap();
55 | else return null;
56 | }
57 | }
58 | public bool IsPrimeEligible { get { return (PrimeLogoImage != null); } }
59 | public String ReviewDistribution
60 | {
61 | get
62 | {
63 | if (_reviewDistribution != null)
64 | {
65 | return _reviewDistribution.ToString();
66 | }
67 | else
68 | {
69 | return null;
70 | }
71 | }
72 | }
73 |
74 | String _name;
75 | int _reviewCount;
76 | DoubleRange _priceRange;
77 | ScoreDistribution _reviewDistribution;
78 | Uri _url;
79 | double _rating;
80 | bool _primeEligible;
81 | BitmapImage _image;
82 |
83 | public AmazonItem(string name,
84 | int reviewCount,
85 | DoubleRange priceRange,
86 | ScoreDistribution reviewDistribution,
87 | Uri url,
88 | double rating,
89 | bool primeEligible,
90 | BitmapImage image)
91 | {
92 | _name = name;
93 | _reviewCount = reviewCount;
94 | _priceRange = priceRange;
95 | _reviewDistribution = reviewDistribution;
96 | _url = url;
97 | _rating = rating;
98 | _primeEligible = primeEligible;
99 | _image = image;
100 |
101 | }
102 |
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/Model/Structure/IValidatable.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace AmazonScrape
8 | {
9 | ///
10 | /// Defines controls that can be validated.
11 | /// Result contains status/error information and an optional generic return value type.
12 | ///
13 | interface IValidatable
14 | {
15 | Result Validate();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Model/Structure/Result.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace AmazonScrape
8 | {
9 | ///
10 | /// Container class for a method result, including any status/error messages and
11 | /// a generic return type.
12 | /// Used in validation and to return results to the main Window.
13 | ///
14 | ///
15 | public class Result
16 | {
17 | public T Value { get { return _returnValue; } set { _returnValue = value; } }
18 | public string StatusMessage { get { return _statusMessage; } set { _statusMessage = value; } }
19 | public string ErrorMessage { get { return _errorMessage; } set { _errorMessage = value; } }
20 | public bool HasReturnValue { get { return ( ! Equals(_returnValue, default(T))); } }
21 | public bool HasError { get { return (_errorMessage.Length > 0); } }
22 |
23 | private T _returnValue;
24 | private string _statusMessage;
25 | private string _errorMessage;
26 |
27 | public Result()
28 | {
29 | _returnValue = default(T);
30 | ErrorMessage = "";
31 | StatusMessage = "";
32 | }
33 |
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Model/Structure/ScoreDistribution.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace AmazonScrape
8 | {
9 | ///
10 | /// Represents the percentage range of values of each Amazon review star category (convenience class)
11 | ///
12 | public class ScoreDistribution
13 | {
14 | public DoubleRange OneStar { get { return _oneStar; } }
15 | public DoubleRange TwoStar { get { return _twoStar; } }
16 | public DoubleRange ThreeStar { get { return _threeStar; } }
17 | public DoubleRange FourStar { get { return _fourStar; } }
18 | public DoubleRange FiveStar { get { return _fiveStar; } }
19 |
20 | private DoubleRange _oneStar;
21 | private DoubleRange _twoStar;
22 | private DoubleRange _threeStar;
23 | private DoubleRange _fourStar;
24 | private DoubleRange _fiveStar;
25 |
26 | public ScoreDistribution(DoubleRange oneStar,
27 | DoubleRange twoStar,
28 | DoubleRange threeStar,
29 | DoubleRange fourStar,
30 | DoubleRange fiveStar)
31 | {
32 | _oneStar = oneStar;
33 | _twoStar = twoStar;
34 | _threeStar = threeStar;
35 | _fourStar = fourStar;
36 | _fiveStar = fiveStar;
37 | }
38 |
39 | public ScoreDistribution(double[] scores)
40 | {
41 | // Ensure that there are five categories being supplied.
42 | if (scores.Length != 5)
43 | {
44 | throw new ArgumentException("Score distribution must have five scores.");
45 | }
46 |
47 | _oneStar = new DoubleRange(scores[0],scores[0]);
48 | _twoStar = new DoubleRange(scores[1], scores[1]);
49 | _threeStar = new DoubleRange(scores[2], scores[2]);
50 | _fourStar = new DoubleRange(scores[3], scores[3]);
51 | _fiveStar = new DoubleRange(scores[4], scores[4]);
52 |
53 | }
54 |
55 | public override string ToString()
56 | {
57 | string results = string.Format("Five star: {0} %", _fiveStar.Low) + Environment.NewLine +
58 | string.Format("Four star: {0} %", _fourStar.Low) + Environment.NewLine +
59 | string.Format("Three star: {0} %", _threeStar.Low) + Environment.NewLine +
60 | string.Format("Two star: {0} %", _twoStar.Low) + Environment.NewLine +
61 | string.Format("One star: {0} %", _oneStar.Low) + Environment.NewLine;
62 |
63 | return results;
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Model/Structure/SearchCriteria.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace AmazonScrape
8 | {
9 | ///
10 | /// Used to pass around the user's search criteria (convenience class)
11 | ///
12 | public class SearchCriteria
13 | {
14 | public string SearchText { get { return _searchText; } }
15 | public double NumberOfResults { get { return _numberOfResults; } }
16 | public double MinNumberReviews { get { return _minNumberReviews; } }
17 | public bool StrictPrimeEligibility { get { return _strictPrimeEligibility; } }
18 | public bool MatchAllSearchTerms { get { return _matchAllSearchTerms; } }
19 | public DoubleRange PriceRange { get { return _priceRange; } }
20 | public ScoreDistribution ScoreDistribution { get { return _distribution; } }
21 |
22 | private string _searchText;
23 | private double _numberOfResults;
24 | private double _minNumberReviews;
25 | private bool _matchAllSearchTerms;
26 | private ScoreDistribution _distribution;
27 | DoubleRange _priceRange;
28 |
29 | // Amazon items that "qualify for super-saver 2 day shipping" are
30 | // almost always Prime Eligible as well. If strictPrimeEligibility is set to false,
31 | // we use this criteria for determining prime eligibility.
32 | // Otherwise, the code goes to each product page and checks for Prime Eligibility.
33 | // While the second method is guaranteed to work, it results in an entire pageload
34 | // for each record, which doubles load times.
35 | private bool _strictPrimeEligibility;
36 |
37 | public SearchCriteria(string searchText,
38 | double numberOfResults,
39 | DoubleRange priceRange,
40 | ScoreDistribution distribution,
41 | double minNumberReviews = 0,
42 | bool matchAllSearchTerms = false,
43 | bool strictPrimeEligibility = false
44 | )
45 | {
46 | _searchText = searchText;
47 | _numberOfResults = numberOfResults;
48 | _matchAllSearchTerms = matchAllSearchTerms;
49 | _minNumberReviews = minNumberReviews;
50 | _strictPrimeEligibility = strictPrimeEligibility;
51 | _distribution = distribution;
52 | _priceRange = priceRange;
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Resources;
3 | using System.Runtime.CompilerServices;
4 | using System.Runtime.InteropServices;
5 | using System.Windows;
6 |
7 | // General Information about an assembly is controlled through the following
8 | // set of attributes. Change these attribute values to modify the information
9 | // associated with an assembly.
10 | [assembly: AssemblyTitle("AmazonScrape")]
11 | [assembly: AssemblyDescription("")]
12 | [assembly: AssemblyConfiguration("")]
13 | [assembly: AssemblyCompany("")]
14 | [assembly: AssemblyProduct("AmazonScrape")]
15 | [assembly: AssemblyCopyright("Copyright © 2012")]
16 | [assembly: AssemblyTrademark("")]
17 | [assembly: AssemblyCulture("")]
18 |
19 | // Setting ComVisible to false makes the types in this assembly not visible
20 | // to COM components. If you need to access a type in this assembly from
21 | // COM, set the ComVisible attribute to true on that type.
22 | [assembly: ComVisible(false)]
23 |
24 | //In order to begin building localizable applications, set
25 | //CultureYouAreCodingWith in your .csproj file
26 | //inside a . For example, if you are using US english
27 | //in your source files, set the to en-US. Then uncomment
28 | //the NeutralResourceLanguage attribute below. Update the "en-US" in
29 | //the line below to match the UICulture setting in the project file.
30 |
31 | //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
32 |
33 |
34 | [assembly: ThemeInfo(
35 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
36 | //(used if a resource is not found in the page,
37 | // or application resource dictionaries)
38 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
39 | //(used if a resource is not found in the page,
40 | // app, or any theme specific resource dictionaries)
41 | )]
42 |
43 |
44 | // Version information for an assembly consists of the following four values:
45 | //
46 | // Major Version
47 | // Minor Version
48 | // Build Number
49 | // Revision
50 | //
51 | // You can specify all the values or you can default the Build and Revision Numbers
52 | // by using the '*' as shown below:
53 | // [assembly: AssemblyVersion("1.0.*")]
54 | [assembly: AssemblyVersion("1.1.0.0")]
55 | [assembly: AssemblyFileVersion("1.0.0.0")]
56 |
--------------------------------------------------------------------------------
/Properties/Resources.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.17929
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace AmazonScrape.Properties
12 | {
13 |
14 |
15 | ///
16 | /// A strongly-typed resource class, for looking up localized strings, etc.
17 | ///
18 | // This class was auto-generated by the StronglyTypedResourceBuilder
19 | // class via a tool like ResGen or Visual Studio.
20 | // To add or remove a member, edit your .ResX file then rerun ResGen
21 | // with the /str option, or rebuild your VS project.
22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
25 | internal class Resources
26 | {
27 |
28 | private static global::System.Resources.ResourceManager resourceMan;
29 |
30 | private static global::System.Globalization.CultureInfo resourceCulture;
31 |
32 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
33 | internal Resources()
34 | {
35 | }
36 |
37 | ///
38 | /// Returns the cached ResourceManager instance used by this class.
39 | ///
40 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
41 | internal static global::System.Resources.ResourceManager ResourceManager
42 | {
43 | get
44 | {
45 | if ((resourceMan == null))
46 | {
47 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AmazonScrape.Properties.Resources", typeof(Resources).Assembly);
48 | resourceMan = temp;
49 | }
50 | return resourceMan;
51 | }
52 | }
53 |
54 | ///
55 | /// Overrides the current thread's CurrentUICulture property for all
56 | /// resource lookups using this strongly typed resource class.
57 | ///
58 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
59 | internal static global::System.Globalization.CultureInfo Culture
60 | {
61 | get
62 | {
63 | return resourceCulture;
64 | }
65 | set
66 | {
67 | resourceCulture = value;
68 | }
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Properties/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | text/microsoft-resx
107 |
108 |
109 | 2.0
110 |
111 |
112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
113 |
114 |
115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
--------------------------------------------------------------------------------
/Properties/Settings.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.17929
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace AmazonScrape.Properties
12 | {
13 |
14 |
15 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
16 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
17 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
18 | {
19 |
20 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
21 |
22 | public static Settings Default
23 | {
24 | get
25 | {
26 | return defaultInstance;
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Properties/Settings.settings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | AmazonScrape
2 | ============
3 |
4 | Amazon Product scraper. Supplies search tools and sortable results (WPF/C#)
5 |
--------------------------------------------------------------------------------
/UI/Controls/DataGridPlus.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Generic;
4 | using System.ComponentModel;
5 | using System.Linq;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 | using System.Windows;
9 | using System.Windows.Controls;
10 | using System.Windows.Data;
11 | using System.Windows.Controls.Primitives;
12 | using System.Windows.Media;
13 |
14 | namespace AmazonScrape
15 | {
16 | ///
17 | /// Extends DataGrid to allow columns to be easily added through code-behind
18 | /// Avoids overly-cluttered XAML
19 | ///
20 | class DataGridPlus : DataGrid
21 | {
22 | // Create dependency properties so we can change the formatting through xaml ( header font size vs. result font size)
23 | public DataGridPlus()
24 | {
25 | RowHeight = Double.NaN;
26 | IsReadOnly = true;
27 | CanUserAddRows = false;
28 | AutoGenerateColumns = true;
29 | CanUserResizeColumns = false;
30 | GridLinesVisibility = DataGridGridLinesVisibility.Horizontal;
31 | CanUserResizeColumns = true;
32 | CanUserReorderColumns = false;
33 | CanUserResizeRows = true;
34 | }
35 |
36 | public void AddImageColumn(string bindingName,
37 | string headerText,
38 | int widthPercent,
39 | BindingMode bindingMode,
40 | string sortOn="",Style style=null)
41 | {
42 | FrameworkElementFactory ef = new FrameworkElementFactory(typeof(Image));
43 | Binding binding = new Binding(bindingName);
44 | binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
45 | binding.Mode = bindingMode;
46 | ef.SetValue(Image.SourceProperty, binding);
47 | AddColumn(ef, widthPercent,headerText,sortOn,style);
48 | }
49 |
50 | public void AddButtonColumn(string buttonText, int widthPercent, RoutedEventHandler clickHandler,Style style = null)
51 | {
52 | FrameworkElementFactory ef = new FrameworkElementFactory(typeof(Button));
53 | ef.SetValue(Button.StyleProperty, ResourceLoader.GetControlStyle("ButtonStyle"));
54 | ef.SetValue(Button.ContentProperty, buttonText);
55 | ef.AddHandler(Button.ClickEvent, clickHandler, true);
56 | AddColumn(ef, widthPercent, "","",style);
57 | }
58 |
59 | public void AddTextColumn(string bindingName,string headerText,int widthPercent,Style style = null)
60 | {
61 | FrameworkElementFactory ef = new FrameworkElementFactory(typeof(TextBlock));
62 | ef.SetValue(TextBlock.TextWrappingProperty, TextWrapping.Wrap);
63 | ef.SetValue(TextBlock.TextAlignmentProperty, TextAlignment.Center);
64 | ef.SetValue(TextBlock.VerticalAlignmentProperty, VerticalAlignment.Center);
65 | ef.SetValue(TextBlock.TextProperty, new Binding(bindingName));
66 | AddColumn(ef, widthPercent, headerText, bindingName, style);
67 | }
68 |
69 | private void AddColumn(FrameworkElementFactory ef,int widthPercent,string headerText,string sortOn = "",Style style=null)
70 | {
71 | // If overriding the default style
72 | // TODO: test this
73 | if (!(style == null)) ef.SetValue(StyleProperty, style);
74 |
75 | DataGridTemplateColumn newCol = new DataGridTemplateColumn();
76 |
77 | if (sortOn.Length > 0)
78 | {
79 | newCol.CanUserSort = true;
80 | newCol.SortMemberPath = sortOn;
81 | }
82 |
83 | newCol.Header = headerText;
84 | newCol.Width = new DataGridLength(widthPercent, DataGridLengthUnitType.Star);
85 |
86 | DataTemplate template = new DataTemplate();
87 | template.VisualTree = ef;
88 | newCol.CellTemplate = template;
89 | this.Columns.Add(newCol);
90 | }
91 |
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/UI/Controls/RangeBoxes.xaml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
24 | Price from
25 |
26 |
27 |
32 |
33 |
36 | to
37 |
38 |
39 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/UI/Controls/RangeBoxes.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Windows.Controls;
3 | namespace AmazonScrape
4 | {
5 | ///
6 | /// Interaction logic for RangeBoxes
7 | ///
8 | [global::System.ComponentModel.TypeConverter(typeof(DoubleRangeConverter))]
9 | public partial class RangeBoxes : UserControl, IValidatable
10 | {
11 | protected string _name;
12 | protected DoubleRange _range = new DoubleRange(); // The allowable range for this control
13 |
14 | public string LowText
15 | {
16 | get
17 | {
18 | return TextLow.Text;
19 | }
20 | set
21 | {
22 | TextLow.Text = value;
23 | }
24 | }
25 |
26 | public string HighText
27 | {
28 | get
29 | {
30 | return TextHigh.Text;
31 | }
32 | set
33 | {
34 | TextHigh.Text = value;
35 | }
36 | }
37 |
38 | public RangeBoxes()
39 | {
40 | InitializeComponent();
41 | }
42 |
43 | public DoubleRange GetValues()
44 | {
45 | DoubleRange values = new DoubleRange();
46 |
47 | if (TextLow.ContainsDoubleValue)
48 | { values.Low = Convert.ToDouble(TextLow.Text); }
49 |
50 | if (TextHigh.ContainsDoubleValue)
51 | { values.High = Convert.ToDouble(TextHigh.Text); }
52 |
53 | return values;
54 | }
55 |
56 | public Result Validate()
57 | {
58 | double low = _range.Low;
59 | double high = _range.High;
60 |
61 | Result result = new Result();
62 |
63 | // Try to cast the contents of the low value to double
64 | if (TextLow.Text.Length > 0)
65 | {
66 | try
67 | {
68 | low = Convert.ToDouble(TextLow.Text);
69 | }
70 | catch (Exception)
71 | {
72 | string message = _name + " low value must be numeric.";
73 | result.ErrorMessage = message;
74 | return result;
75 | }
76 | }
77 |
78 | // Try to cast the contents of the high value to double
79 | if (TextHigh.Text.Length > 0)
80 | {
81 | try
82 | { high = Convert.ToDouble(TextHigh.Text); }
83 | catch (Exception)
84 | {
85 | string message = _name + " high value must be numeric.";
86 | result.ErrorMessage = message;
87 | return result;
88 | }
89 | }
90 |
91 | if (!_range.Contains(low) || !_range.Contains(high))
92 | {
93 | string message = "Specified " + _name + " values are out of allowable range.";
94 | result.ErrorMessage = message;
95 | return result;
96 | }
97 |
98 | if (low > high)
99 | {
100 | string message = _name + " low value is greater than high value.";
101 | result.ErrorMessage = message;
102 | return result;
103 | }
104 |
105 | return result;
106 | }
107 |
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/UI/Controls/RangeSlider.xaml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | to
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/UI/Controls/RangeSlider.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 | using System.Windows.Controls;
3 | using System.Windows.Data;
4 | namespace AmazonScrape
5 | {
6 | ///
7 | /// Interaction logic for RangeSlider.xaml
8 | ///
9 | public partial class RangeSlider : UserControl
10 | {
11 | public static readonly DependencyProperty HeaderTextProperty = DependencyProperty.Register
12 | (
13 | "HeaderText",
14 | typeof(string),
15 | typeof(RangeSlider),
16 | new PropertyMetadata(string.Empty)
17 | );
18 |
19 | public string HeaderText
20 | {
21 | get { return (string)GetValue(HeaderTextProperty); }
22 | set { SetValue(HeaderTextProperty, value); }
23 | }
24 |
25 | public RangeSlider()
26 | {
27 | InitializeComponent();
28 |
29 | LowText.textBox.Text = "0";
30 | HighText.textBox.Text = "100";
31 |
32 | // Set bindings for the sliders to change the values of the
33 | // text boxes
34 | Binding b = new Binding();
35 | b.Source = LowSlider;
36 | b.Path = new PropertyPath("Value", LowSlider.Value);
37 | b.Mode = BindingMode.TwoWay;
38 | b.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
39 | LowText.textBox.SetBinding(TextBox.TextProperty, b);
40 |
41 | b = new Binding();
42 | b.Source = HighSlider;
43 | b.Path = new PropertyPath("Value", HighSlider.Value);
44 | b.Mode = BindingMode.TwoWay;
45 | b.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
46 | HighText.textBox.SetBinding(TextBox.TextProperty, b);
47 |
48 | // Check to ensure that the low slider doesn't exceed the high slider
49 | LowSlider.ValueChanged += LowSlider_ValueChanged;
50 | HighSlider.ValueChanged += HighSlider_ValueChanged;
51 | }
52 |
53 | public double Low { get { return LowSlider.Value; } set { LowSlider.Value = value; } }
54 | public double High { get { return HighSlider.Value; } set { HighSlider.Value = value; } }
55 |
56 | // The range of possible values for the sliders
57 | protected DoubleRange range;
58 |
59 | public delegate void LowValueChangedEventHandler(RangeSlider slider, double oldValue, double newValue);
60 | public event LowValueChangedEventHandler LowValueChanged;
61 |
62 | void HighSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
63 | {
64 | if (e.NewValue < LowSlider.Value)
65 | {
66 | LowSlider.Value = e.NewValue;
67 | }
68 | }
69 |
70 | void LowSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
71 | {
72 | if (e.NewValue > HighSlider.Value)
73 | {
74 | HighSlider.Value = e.NewValue;
75 | }
76 |
77 | if (this.LowValueChanged != null)
78 | {
79 | LowValueChanged(this, e.OldValue, e.NewValue);
80 | }
81 | }
82 |
83 | public DoubleRange GetRange()
84 | {
85 | return new DoubleRange(LowSlider.Value, HighSlider.Value);
86 | }
87 |
88 | } // class
89 | } // namespace
90 |
--------------------------------------------------------------------------------
/UI/Controls/ScoreDistributionControl.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using System.Windows;
7 | using System.Windows.Controls;
8 | using System.Windows.Data;
9 | using System.Windows.Media;
10 |
11 | namespace AmazonScrape
12 | {
13 | class ScoreDistributionControl : GridPlus, IValidatable
14 | {
15 | ///
16 | /// Provides "low range" and "high range" sliders for each Amazon review
17 | /// category. Allows the user to supply the desired minimum and maximum
18 | /// percentage values for the product results.
19 | ///
20 | /// Examples:
21 | ///
22 | /// If the user specifies a 5% high range value for one-star reviews,
23 | /// no more than 5% of all reviews will be one-star reviews.
24 | ///
25 | /// If the user specifies a 50% low range value for five-star reviews,
26 | /// no fewer than 50% of all reviews will be five-star reviews.
27 | ///
28 | public DoubleRange OneStarRange { get { return _sliders[0].GetRange(); } }
29 | public DoubleRange TwoStarRange { get { return _sliders[1].GetRange(); } }
30 | public DoubleRange ThreeStarRange { get { return _sliders[2].GetRange(); } }
31 | public DoubleRange FourStarRange { get { return _sliders[3].GetRange(); } }
32 | public DoubleRange FiveStarRange { get { return _sliders[4].GetRange(); } }
33 | public ScoreDistribution Distribution
34 | {
35 | get
36 | {
37 | return new ScoreDistribution(OneStarRange,
38 | TwoStarRange,
39 | ThreeStarRange,
40 | FourStarRange,
41 | FiveStarRange);
42 | }
43 | }
44 |
45 | // One RangeSlider for each star-rating category
46 | private RangeSliderX[] _sliders = new RangeSliderX[5];
47 |
48 | // The control prevents the user from setting the "low" ranges in such
49 | // a way that the total exceeds 100%. When a user tries to raise a value
50 | // that woudld cause the total to exceed 100%, it reduces the other control
51 | // values accordingly. This boolean prevents an event cascade when the other
52 | // control values are being set.
53 | bool resolvingPercentageError = false;
54 |
55 | public ScoreDistributionControl()
56 | {
57 | // Five columns, one for each star category RangeSlider
58 | //ColumnDefinition col;
59 | for (int i = 0; i < 5; i++)
60 | {
61 | AddColumn(20, GridUnitType.Star);
62 | }
63 |
64 | // Two rows, one for the header and one for the controls
65 | AddRow(10, GridUnitType.Star);
66 | AddRow(90, GridUnitType.Star);
67 |
68 | // Control header
69 | TextBlock text = new TextBlock();
70 | text.Background = new SolidColorBrush(Colors.LightGray);
71 | ToolTip tip = new ToolTip();
72 | tip.Content = "Specify how the results should be distributed." + Environment.NewLine;
73 | tip.Content += "For instance, if you set the one-star 'high' slider to 5%, it means that" + Environment.NewLine;
74 | tip.Content += "the returned items will have no more than 5% one-star reviews.";
75 | text.ToolTip = tip;
76 | text.Foreground = new SolidColorBrush(Colors.Blue);
77 | text.Text = "Result percentage ranges per star category";
78 | text.TextAlignment = TextAlignment.Center;
79 |
80 | AddContent(text, 0, 0, 5);
81 |
82 | // Five range sliders; one for each star category
83 | for (int i = 0; i < 5; i++)
84 | {
85 | RangeSliderX slider = new RangeSliderX();
86 | slider.HeaderText = (i+1).ToString() + " - Star";
87 | slider.High = 100;
88 | slider.Low = 0;
89 | slider.VerticalAlignment = VerticalAlignment.Stretch;
90 | slider.HorizontalAlignment = HorizontalAlignment.Stretch;
91 | _sliders[i] = slider;
92 | //_sliders[i] = new RangeSliderX((i+1).ToString() + " - Star", new DoubleRange(0, 100))
93 | //{
94 | // VerticalAlignment = System.Windows.VerticalAlignment.Stretch,
95 | // HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
96 | //};
97 | _sliders[i].LowValueChanged += slider_LowValueChanged;
98 |
99 | AddContent(_sliders[i], 1, i, 1);
100 | }
101 | }
102 |
103 | ///
104 | /// Returns the sum total of "low" range scores, given a RangeSlider
105 | /// that just had its value modified.
106 | ///
107 | ///
108 | ///
109 | /// Sum of current "low" values
110 | double GetNewSumOfLowRange(RangeSliderX sender,double newValue)
111 | {
112 | // Start with the supplied new value
113 | double total = newValue;
114 | for (int i = 0; i < 5; i++)
115 | {
116 | if (_sliders[i] != sender)
117 | {
118 | total += _sliders[i].Low;
119 | }
120 | }
121 | return total;
122 | }
123 |
124 |
125 | ///
126 | /// Given a RangeSlider, provides a list of the other RangeSliders that have
127 | /// a positive "low" range value. Used when adjusting the total of the "low"
128 | /// range sliders to not exceed 100%.
129 | ///
130 | ///
131 | ///
132 | List GetOtherSlidersWithPositiveLowScore(RangeSliderX sender)
133 | {
134 | List results = new List();
135 |
136 | // Loop through and get the sliders (besides the sender)
137 | // that have a positive "low" range value.
138 | for (int i = 0; i < 5; i++)
139 | {
140 | if (_sliders[i] != sender && _sliders[i].Low > 0)
141 | { results.Add(_sliders[i]); }
142 | }
143 | return results;
144 | }
145 |
146 | ///
147 | /// Prevents user from specifying a total minimum number of results that exceeds 100%
148 | /// by dynamically reducing the values of the other sliders.
149 | ///
150 | /// e.g. the user specifies 40% for the "low" range value of each of the five
151 | /// star categories. The sum would be 200% and wouldn't make sense.
152 | ///
153 | ///
154 | ///
155 | void slider_LowValueChanged(RangeSliderX sender, double oldValue, double newValue)
156 | {
157 | // If we're already dealing with with this problem, allow it to resolve.
158 | // This prevents a cascade of events as every change to a low score causes
159 | // this method to be called
160 | if (resolvingPercentageError) { return; }
161 |
162 | // Figure out whether the change puts the "low" sum over 100%
163 | double sum = GetNewSumOfLowRange(sender, newValue);
164 |
165 | if (sum <= 100) { return; }
166 |
167 | resolvingPercentageError = true;
168 |
169 | // Loop through the sliders, reducing each until
170 | // the total amount is less than or equal to 100
171 | while (sum > 100)
172 | {
173 | // We can't do anything with sliders with a zero "low" value, so get
174 | // a list of other sliders that have a non-zero "low" value
175 | List hasLowRangeValue = GetOtherSlidersWithPositiveLowScore(sender);
176 |
177 | foreach (RangeSliderX rangeSlider in hasLowRangeValue)
178 | {
179 | // how much more we have to distribute
180 | double amountAboveOneHundred = sum - 100;
181 |
182 | // The amount over 100 divided by the number of sliders that have
183 | // a "low" value.
184 | double shareOfAmountOver = Math.Ceiling(amountAboveOneHundred / hasLowRangeValue.Count);
185 |
186 | // Try to subtract this amount from the current slider
187 | double remainder = rangeSlider.Low - Math.Ceiling(amountAboveOneHundred / hasLowRangeValue.Count);
188 |
189 | // Two possibilities:
190 | // 1) The slider had enough "low" value to subtract its share of the overage
191 | if (remainder > 0)
192 | {
193 | sum -= shareOfAmountOver;
194 | rangeSlider.Low = remainder;
195 | }
196 | else // 2) We could only subtract a part of this slider's share before it became zero
197 | {
198 | // We've reduced this slider's "low" value to zero
199 | rangeSlider.Low = 0;
200 |
201 | // The negative remainder represents the amount that we couldn't subtract
202 | // from this slider. Adjust sum to reflect the portion that we subtracted
203 | sum -= (shareOfAmountOver - Math.Abs(remainder));
204 |
205 | }
206 |
207 | } // for each rangeslider with a low value
208 |
209 | } // sum > 100
210 |
211 | // The problem has been successfully resolved.
212 | resolvingPercentageError = false;
213 | }
214 |
215 | ///
216 | /// Returns whether the control is validated
217 | /// Always returns true, as the control ensures valid values.
218 | ///
219 | ///
220 | public ValidationResult Validate()
221 | {
222 | return new ValidationResult(ValidationResult.Status.Valid);
223 | }
224 | }
225 | }
226 |
--------------------------------------------------------------------------------
/UI/Controls/ScoreDistributionControl.xaml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
25 | Result Percentage per star category
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
44 |
45 |
52 |
53 |
60 |
61 |
68 |
69 |
76 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/UI/Controls/ScoreDistributionControl.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Windows.Controls;
4 | using System.Windows.Controls.Primitives;
5 | using System.Windows.Media;
6 |
7 | namespace AmazonScrape
8 | {
9 | ///
10 | /// Interaction logic for ScoreDistribution.xaml
11 | ///
12 | public partial class ScoreDistributionControl : UserControl
13 | {
14 | // The control prevents the user from setting the "low" ranges in such
15 | // a way that the total exceeds 100%. When a user tries to raise a value
16 | // that woudld cause the total to exceed 100%, it reduces the other control
17 | // values accordingly. This boolean prevents an event cascade when the other
18 | // control values are being set.
19 | bool resolvingPercentageError = false;
20 | private RangeSlider[] _sliders;
21 | private Popup _description;
22 |
23 | public ScoreDistribution Distribution
24 | {
25 | get
26 | {
27 | return new ScoreDistribution(
28 | OneStar.GetRange(),
29 | TwoStar.GetRange(),
30 | ThreeStar.GetRange(),
31 | FourStar.GetRange(),
32 | FiveStar.GetRange());
33 | }
34 | }
35 |
36 | public ScoreDistributionControl()
37 | {
38 | InitializeComponent();
39 |
40 | // Makes validating the values of the sliders easier
41 | _sliders = new RangeSlider[5];
42 | _sliders[0] = OneStar;
43 | _sliders[1] = TwoStar;
44 | _sliders[2] = ThreeStar;
45 | _sliders[3] = FourStar;
46 | _sliders[4] = FiveStar;
47 |
48 | string description = "Usage examples: " + Environment.NewLine + Environment.NewLine +
49 | " - If you wanted products that had no more than 10% one-star " + Environment.NewLine +
50 | " reviews, you would set the one-star review control range to " + Environment.NewLine +
51 | " 0 to 10." +
52 | Environment.NewLine + Environment.NewLine +
53 | " - If you wanted products that had no fewer than 40% five-star" + Environment.NewLine +
54 | " reviews, you would set the five-star review control range to " + Environment.NewLine +
55 | " 40 to 100.";
56 |
57 |
58 | _description = new Popup();
59 | _description.PlacementTarget = ControlTitle;
60 | _description.Placement = PlacementMode.Left;
61 | var textBlock = new TextBlock();
62 | textBlock.Text = description;
63 | textBlock.Foreground = new SolidColorBrush(Colors.Black);
64 | textBlock.Background = new SolidColorBrush(Colors.White);
65 | _description.Child = textBlock;
66 |
67 | ControlTitle.MouseEnter += ControlTitle_MouseEnter;
68 | ControlTitle.MouseLeave += ControlTitle_MouseLeave;
69 |
70 | }
71 |
72 | void ControlTitle_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
73 | {
74 | _description.IsOpen = true;
75 | }
76 |
77 | void ControlTitle_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
78 | {
79 | _description.IsOpen = false;
80 | }
81 |
82 |
83 |
84 |
85 | ///
86 | /// Returns the sum total of "low" range scores, given a RangeSlider
87 | /// that just had its value modified.
88 | ///
89 | ///
90 | ///
91 | /// Sum of current "low" values
92 | double GetNewSumOfLowRange(RangeSlider sender, double newValue)
93 | {
94 | // Start with the supplied new value
95 | double total = newValue;
96 |
97 | for (int i = 0; i < 5; i++)
98 | {
99 | if (_sliders[i] != sender)
100 | {
101 | total += _sliders[i].Low;
102 | }
103 | }
104 | return total;
105 | }
106 |
107 | ///
108 | /// Given a RangeSlider, provides a list of the other RangeSliders that have
109 | /// a positive "low" range value. Used when adjusting the total of the "low"
110 | /// range sliders to not exceed 100%.
111 | ///
112 | ///
113 | ///
114 | List GetOtherSlidersWithPositiveLowScore(RangeSlider sender)
115 | {
116 | List results = new List();
117 |
118 | // Loop through and get the sliders (besides the sender)
119 | // that have a positive "low" range value.
120 | for (int i = 0; i < 5; i++)
121 | {
122 | if (_sliders[i] != sender && _sliders[i].Low > 0)
123 | { results.Add(_sliders[i]); }
124 | }
125 | return results;
126 | }
127 |
128 | ///
129 | /// Prevents user from specifying a total minimum number of results that exceeds 100%
130 | /// by dynamically reducing the values of the other sliders.
131 | ///
132 | /// e.g. the user specifies 40% for the "low" range value of each of the five
133 | /// star categories. The sum would be 200% and wouldn't make sense.
134 | ///
135 | ///
136 | ///
137 | void slider_LowValueChanged(RangeSlider sender, double oldValue, double newValue)
138 | {
139 | // If we're already dealing with with this problem, allow it to resolve.
140 | // This prevents a cascade of events as every change to a low score causes
141 | // this method to be called
142 | if (resolvingPercentageError) { return; }
143 |
144 | // Figure out whether the change puts the "low" sum over 100%
145 | double sum = GetNewSumOfLowRange(sender, newValue);
146 |
147 | if (sum <= 100) { return; }
148 |
149 | resolvingPercentageError = true;
150 |
151 | // Loop through the sliders, reducing each until
152 | // the total amount is less than or equal to 100
153 | while (sum > 100)
154 | {
155 | // We can't do anything with sliders with a zero "low" value, so get
156 | // a list of other sliders that have a non-zero "low" value
157 | List hasLowRangeValue = GetOtherSlidersWithPositiveLowScore(sender);
158 |
159 | foreach (RangeSlider rangeSlider in hasLowRangeValue)
160 | {
161 | // how much more we have to distribute
162 | double amountAboveOneHundred = sum - 100;
163 |
164 | // The amount over 100 divided by the number of sliders that have
165 | // a "low" value.
166 | double shareOfAmountOver = Math.Ceiling(amountAboveOneHundred / hasLowRangeValue.Count);
167 |
168 | // Try to subtract this amount from the current slider
169 | double remainder = rangeSlider.Low - Math.Ceiling(amountAboveOneHundred / hasLowRangeValue.Count);
170 |
171 | // Two possibilities:
172 | // 1) The slider had enough "low" value to subtract its share of the overage
173 | if (remainder > 0)
174 | {
175 | sum -= shareOfAmountOver;
176 | rangeSlider.Low = remainder;
177 | }
178 | else // 2) We could only subtract a part of this slider's share before it became zero
179 | {
180 | // We've reduced this slider's "low" value to zero
181 | rangeSlider.Low = 0;
182 |
183 | // The negative remainder represents the amount that we couldn't subtract
184 | // from this slider. Adjust sum to reflect the portion that we subtracted
185 | sum -= (shareOfAmountOver - Math.Abs(remainder));
186 |
187 | }
188 |
189 | } // for each rangeslider with a low value
190 |
191 | } // sum > 100
192 |
193 | // The problem has been successfully resolved.
194 | resolvingPercentageError = false;
195 | }
196 |
197 | ///
198 | /// Returns whether the control is validated
199 | /// Always returns true, as the control ensures valid values.
200 | ///
201 | ///
202 | public Result Validate()
203 | {
204 | return new Result();
205 | }
206 |
207 |
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/UI/Controls/TextBoxPlus.xaml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/UI/Controls/TextBoxPlus.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Windows;
3 | using System.Windows.Controls;
4 | using System.Windows.Data;
5 | using System.Windows.Input;
6 | using System.Windows.Media;
7 |
8 | namespace AmazonScrape
9 | {
10 | ///
11 | /// Interaction logic for TextBoxPlus.xaml
12 | ///
13 | [global::System.ComponentModel.TypeConverter(typeof(DoubleRangeConverter))]
14 | public partial class TextBoxPlus : UserControl, IValidatable
15 | {
16 | public TextBoxPlus()
17 | {
18 | Focusable = true;
19 | InitializeComponent();
20 |
21 | LostFocus += TextBoxPlus_LostFocus;
22 | PreviewGotKeyboardFocus += TextBoxPlus_PreviewGotKeyboardFocus;
23 | GotFocus += TextBoxPlus_GotFocus;
24 | }
25 |
26 | private void TextBoxPlus_GotFocus(object sender, RoutedEventArgs e)
27 | {
28 | textBox.SelectAll();
29 | }
30 |
31 | void TextBoxPlus_PreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
32 | {
33 | if (e.OriginalSource == sender)
34 | {
35 | textBox.Focus();
36 | e.Handled = true;
37 | }
38 | }
39 |
40 | public string Title { get { return _title; } set { _title = value; } }
41 | public bool ContainsDoubleValue { get { return HasDoubleValue(); } }
42 | private string _title = ""; // What to call the control if validation fails.
43 |
44 | public static readonly DependencyProperty TextProperty = DependencyProperty.Register
45 | (
46 | "Text",
47 | typeof(string),
48 | typeof(TextBoxPlus),
49 | new PropertyMetadata(string.Empty)
50 | );
51 |
52 | public string Text
53 | {
54 | get
55 | {
56 | // Force the textbox to grab the current value
57 | // This prevents stale data from being returned
58 | BindingExpression be = textBox.GetBindingExpression(TextBox.TextProperty);
59 | be.UpdateSource();
60 | return (string)GetValue(TextProperty);
61 | }
62 | set { SetValue(TextProperty, value); }
63 | }
64 |
65 | public static readonly DependencyProperty RequiresValueProperty = DependencyProperty.Register
66 | (
67 | "RequiresValue",
68 | typeof(bool),
69 | typeof(TextBoxPlus),
70 | new PropertyMetadata(false)
71 | );
72 |
73 | public bool RequiresValue
74 | {
75 | get { return (bool)GetValue(RequiresValueProperty); }
76 | set { SetValue(RequiresValueProperty, value); }
77 | }
78 |
79 | public static readonly DependencyProperty IsNumericOnlyProperty = DependencyProperty.Register
80 | (
81 | "NumericOnly",
82 | typeof(bool),
83 | typeof(TextBoxPlus),
84 | new PropertyMetadata(false)
85 | );
86 |
87 | public bool IsNumericOnly
88 | {
89 | get { return (bool)GetValue(IsNumericOnlyProperty); }
90 | set { SetValue(IsNumericOnlyProperty, value); }
91 | }
92 |
93 | public static readonly DependencyProperty NumericRangeProperty = DependencyProperty.Register
94 | (
95 | "NumericRange",
96 | typeof(DoubleRange),
97 | typeof(TextBoxPlus),
98 | new PropertyMetadata(null)
99 | );
100 |
101 | public DoubleRange NumericRange
102 | {
103 | get { return (DoubleRange)GetValue(NumericRangeProperty); }
104 | set { IsNumericOnly = true; SetValue(NumericRangeProperty, value); }
105 | }
106 |
107 | // Specify a numeric value range that the text must fall within
108 | // Otherwise, cause a validation error in Validate()
109 | //private DoubleRange _numericRange;
110 | //private string _name;
111 | protected bool HasDoubleValue()
112 | {
113 | double result;
114 | if (Double.TryParse(textBox.Text, out result))
115 | {
116 | return true;
117 | }
118 | return false;
119 | }
120 |
121 |
122 | ///
123 | /// Type converter for range values.
124 | /// Range should be in the form of: "X,Y" where X is the numeric low value and Y is the numeric high value
125 | /// If one of the range values is blank, it means that there is no boundary on that end of the range.
126 | /// For example, "X," would mean X is the low value, and the top end of the range is the max value for a double.
127 | /// Note: A comma is required even if no values are being supplied.
128 | ///
129 | ///
130 | ///
131 | public static DoubleRange Parse(string data)
132 | {
133 | return DoubleRange.Parse(data);
134 | }
135 |
136 | void TextBoxPlus_OnGotFocus(object sender, EventArgs e)
137 | {
138 | textBox.SelectAll();
139 | }
140 |
141 | void TextBoxPlus_LostFocus(object sender, System.Windows.RoutedEventArgs e)
142 | { this.Background = new SolidColorBrush(Color.FromRgb(255, 255, 255)); }
143 |
144 | public Result Validate()
145 | {
146 | Result result = new Result();
147 |
148 | // Error if blank and a value is required
149 | if (RequiresValue && textBox.Text.Length == 0)
150 | {
151 | result.ErrorMessage = _title + " cannot be blank.";
152 | return result;
153 | }
154 |
155 | if (IsNumericOnly)
156 | {
157 | double value;
158 | try
159 | {
160 | value = Convert.ToDouble(textBox.Text);
161 | }
162 | catch
163 | {
164 | result.ErrorMessage = _title + " is not numeric or exceeds the allowable numeric range.";
165 | return result;
166 | }
167 |
168 | DoubleRange range = NumericRange;
169 | if (range != null)
170 | {
171 | if (!range.Contains(value))
172 | {
173 | result.ErrorMessage = _title + " must be " + range.ToString();
174 | }
175 | }
176 | }
177 | return result;
178 | } // validate
179 |
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/UI/Controls/ValueRangeControl.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using System.Windows;
7 | using System.Windows.Controls;
8 |
9 | namespace AmazonScrape
10 |
11 | {
12 | class ValueRangeControl : Grid
13 | {
14 | DoubleRange _range;
15 |
16 | public ValueRangeControl(string name, DoubleRange range)
17 | {
18 | this._range = range;
19 |
20 | // Set up grid with four coulmns, one row
21 | ColumnDefinition col = new ColumnDefinition();
22 | col.Width = new GridLength(20, GridUnitType.Star);
23 | this.ColumnDefinitions.Add(col);
24 |
25 | col = new ColumnDefinition();
26 | col.Width = new GridLength(20, GridUnitType.Star);
27 | this.ColumnDefinitions.Add(col);
28 |
29 | col = new ColumnDefinition();
30 | col.Width = new GridLength(20, GridUnitType.Star);
31 | this.ColumnDefinitions.Add(col);
32 |
33 | col = new ColumnDefinition();
34 | col.Width = new GridLength(20, GridUnitType.Star);
35 | this.ColumnDefinitions.Add(col);
36 |
37 | RowDefinition row = new RowDefinition();
38 | row.Height = new GridLength(100, GridUnitType.Star);
39 |
40 | // TODO: remove the set row if it's not necessary
41 | TextBlock from = new TextBlock();
42 | from.Text = name + " from :";
43 | this.Children.Add(from);
44 | Grid.SetColumn(from,0);
45 | Grid.SetRow(from,0);
46 |
47 | TextBoxPlus low = new TextBoxPlus();
48 | low.Text = range.Low.ToString();
49 | this.Children.Add(low);
50 | Grid.SetColumn(low, 1);
51 | Grid.SetRow(low,0);
52 |
53 | TextBlock to = new TextBlock();
54 | to.Text = " to : ";
55 | this.Children.Add(to);
56 | Grid.SetColumn(to, 2);
57 | Grid.SetRow(to, 0);
58 |
59 | TextBoxPlus high = new TextBoxPlus();
60 | if (range.High < double.MaxValue)
61 | { high.Text = range.High.ToString(); }
62 | Grid.SetColumn(high, 3);
63 | Grid.SetRow(high, 0);
64 |
65 |
66 | }
67 |
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/UI/MainWindow.xaml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
49 |
50 |
51 |
58 |
59 |
60 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
96 |
97 |
98 |
102 | Search
103 |
104 |
105 |
106 |
117 |
118 |
119 |
120 |
130 |
131 | Match all terms
136 |
137 |
138 |
145 |
146 |
150 | # of results
151 |
152 |
153 |
166 |
167 |
171 | Min # of reviews
172 |
173 |
174 |
187 |
188 |
196 |
197 |
206 |
207 |
212 |
213 |
214 |
217 |
220 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
255 |
256 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
--------------------------------------------------------------------------------
/UI/MainWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.ComponentModel;
4 | using System.Diagnostics;
5 | using System.Reflection;
6 | using System.Threading;
7 | using System.Windows;
8 | using System.Windows.Controls;
9 | using System.Windows.Data;
10 | using System.Windows.Input;
11 | using System.Windows.Media;
12 | using System.Windows.Media.Animation;
13 | using System.Windows.Threading;
14 |
15 | namespace AmazonScrape
16 | {
17 | ///
18 | /// Interaction logic for MainWindow.xaml
19 | ///
20 | public partial class MainWindow : Window
21 | {
22 | private SearchManager _searchManager; // Oversees product searches
23 | private List requireValidation; // list of controls that require validation
24 |
25 | public MainWindow()
26 | {
27 | // Catch any vague XAML exceptions
28 | try { InitializeComponent(); }
29 | catch
30 | {
31 | MessageBox.Show("XAML initialization error.");
32 | }
33 |
34 | Icon = ResourceLoader.GetProgramIconBitmap();
35 | Title = "AmazonScrape";
36 |
37 | Version version = Assembly.GetExecutingAssembly().GetName().Version;
38 | Title += " " + version;
39 | WindowState = System.Windows.WindowState.Maximized;
40 |
41 | // Specify the controls requiring validation
42 | // (validation properties are set in XAML)
43 | requireValidation = new List();
44 | requireValidation.Add(txtSearch);
45 | requireValidation.Add(txtNumberOfResults);
46 | requireValidation.Add(txtMinNumberOfReviews);
47 | requireValidation.Add(PriceRange);
48 |
49 | Style smallTextStyle = ResourceLoader.GetControlStyle("DataGridSmallTextStyle");
50 | Style mediumTextStyle = ResourceLoader.GetControlStyle("DataGridMediumTextStyle");
51 | Style largeTextStyle = ResourceLoader.GetControlStyle("DataGridLargeTextStyle");
52 |
53 | // Specify the result grid format
54 | ResultGrid.ColumnHeaderHeight = 40;
55 | ResultGrid.MouseDoubleClick += dataGrid_MouseDoubleClick;
56 | ResultGrid.PreviewMouseLeftButtonDown += dataGrid_PreviewMouseLeftButtonDown;
57 | ResultGrid.AddImageColumn("ProductImage", "Product", 5, BindingMode.TwoWay);
58 | ResultGrid.AddImageColumn("PrimeLogoImage", "Prime", 4, BindingMode.OneWay,"IsPrimeEligible");
59 | ResultGrid.AddTextColumn("Name", "Product Name", 13, mediumTextStyle);
60 | ResultGrid.AddTextColumn("LowPrice", "Low Price", 5, largeTextStyle);
61 | ResultGrid.AddTextColumn("HighPrice", "High Price", 5, largeTextStyle);
62 | ResultGrid.AddTextColumn("Rating", "Rating", 3, largeTextStyle);
63 | ResultGrid.AddTextColumn("ReviewCount", "Reviews", 5, largeTextStyle);
64 | ResultGrid.AddTextColumn("ReviewDistribution", "Distribution", 5,smallTextStyle);
65 | ResultGrid.AddButtonColumn("Open", 3, new RoutedEventHandler(OpenInBrowser_Click));
66 |
67 | // Set focus to the search control once the window is loaded
68 | this.Loaded += MainWindow_Loaded;
69 | }
70 |
71 | void SearchControl_KeyDown(object sender, KeyEventArgs e)
72 | {
73 | if (e.Key == Key.Enter)
74 | {
75 | Search();
76 | }
77 | }
78 |
79 | void MainWindow_Loaded(object sender, RoutedEventArgs e)
80 | {
81 | txtSearch.Focus();
82 | }
83 |
84 | ///
85 | /// If the user clicks in a data grid area that is not a result, remove the
86 | /// grid selection.
87 | ///
88 | /// This is to prevent the user from being able to double click
89 | /// an empty part of the data grid to open a selected item in a browser tab.
90 | /// The intended behavior is to only open an item in a browser if the user
91 | /// double-clicks directly on a data grid result.
92 | ///
93 | ///
94 | void dataGrid_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
95 | {
96 | IInputElement element = e.MouseDevice.DirectlyOver;
97 | if (element != null && element is FrameworkElement)
98 | {
99 | // If the element selected is of type scroll viewer, it means that the
100 | // user is not clicking on a data grid result. In that case, remove
101 | // the current selection
102 | if (element.GetType() == typeof(System.Windows.Controls.ScrollViewer))
103 | { ResultGrid.SelectedIndex = -1; }
104 | }
105 | }
106 |
107 | ///
108 | /// Double clicking a result item will attempt to open the item's URL
109 | /// in whatever program they have associated with URLs.
110 | ///
111 | ///
112 | ///
113 | void dataGrid_MouseDoubleClick(object sender, MouseButtonEventArgs e)
114 | {
115 | // If the user is clicking the scrollbar (the arrow
116 | // or the "thumb"), don't attempt to open an item in the browser
117 | IInputElement element = e.MouseDevice.DirectlyOver;
118 | if (element != null && element is FrameworkElement)
119 | {
120 | var elementType = element.GetType();
121 |
122 | if (elementType == typeof(System.Windows.Controls.Primitives.RepeatButton) ||
123 | elementType == typeof(System.Windows.Controls.Primitives.Thumb))
124 | { return; }
125 | }
126 |
127 | if (ResultGrid.SelectedItem == null) return;
128 | AmazonItem item = ResultGrid.SelectedItem as AmazonItem;
129 | if (item.URL == null)
130 | {
131 | MessageBox.Show("The item's URL cannot be parsed.");
132 | return;
133 | }
134 | OpenWebpage(item.URL.ToString());
135 | }
136 |
137 | ///
138 | /// Attempts to open the user's default web browser and direct
139 | /// it to the supplied url.
140 | ///
141 | ///
142 | private void OpenWebpage(string url)
143 | {
144 | // TODO: reported bug that this doesn't work on a machine that
145 | // does not have framework 4.5 installed. Get more details.
146 | try
147 | {
148 | System.Diagnostics.Process.Start(url);
149 | }
150 | catch (Exception)
151 | {
152 | MessageBox.Show("Error while trying to open browser for the requested product.");
153 | }
154 | }
155 |
156 | ///
157 | /// Obtains the selected result item's URL and attempts to open
158 | /// the user's default web browser to that page.
159 | ///
160 | ///
161 | ///
162 | private void OpenInBrowser_Click(object sender, RoutedEventArgs e)
163 | {
164 | AmazonItem item = ((FrameworkElement)sender).DataContext as AmazonItem;
165 |
166 | if (item.URL == null)
167 | {
168 | MessageBox.Show("The item's URL cannot be parsed.");
169 | return;
170 | }
171 | OpenWebpage(item.URL.ToString());
172 | }
173 |
174 | ///
175 | /// Cancels an ongoing search.
176 | ///
177 | /// Because the search is being processed by a BackgroundWorker thread,
178 | /// the cancellation will not be instantaneous.
179 | ///
180 | ///
181 | void btnCancel_Click(object sender, RoutedEventArgs e)
182 | {
183 | StatusTextBox.Text += Environment.NewLine + Environment.NewLine;
184 | StatusTextBox.Text += "Canceling search! Please be patient.";
185 | CancelSearch();
186 | }
187 |
188 | void textBox_KeyDown(object sender, KeyEventArgs e)
189 | {
190 | // Search if the user presses the enter key on the search box
191 | if (e.Key == Key.Enter) Search();
192 | }
193 |
194 | ///
195 | /// Loop through each control marked as validatable and check its
196 | /// validation status. If a single control is not valid (Status.Error),
197 | /// validation stops and the ValidationResult is immediately returned.
198 | ///
199 | /// ValidationResult
200 | private Result ValidateControls()
201 | {
202 | Result result = new Result();
203 |
204 | foreach (IValidatable validatable in requireValidation)
205 | {
206 | result = validatable.Validate();
207 | if (result.ErrorMessage.Length > 0) return result;
208 | }
209 | return result;
210 | }
211 |
212 | ///
213 | /// Obtains the current control values and returns a SearchCriteria object.
214 | /// Note: does not validate the controls; perform validation before calling.
215 | ///
216 | ///
217 | private SearchCriteria GetSearchCriteria()
218 | {
219 | DoubleRange priceRange = PriceRange.GetValues();
220 |
221 | double minReviewCount = Convert.ToDouble(txtMinNumberOfReviews.Text);
222 |
223 | return new SearchCriteria(txtSearch.Text,
224 | Convert.ToDouble(txtNumberOfResults.Text),
225 | priceRange,
226 | ScoreDistribution.Distribution,
227 | minReviewCount,
228 | (bool)chkMatchAll.IsChecked,
229 | Constants.USE_STRICT_PRIME_ELIGIBILITY);
230 | }
231 |
232 | ///
233 | /// Validates controls, obtains the control values, and begins the
234 | /// asynchronous scraping / parsing / result handling process.
235 | ///
236 | private void Search()
237 | {
238 | // Validate the search controls.
239 | Result result = ValidateControls();
240 |
241 | if (result.ErrorMessage.Length > 0)
242 | {
243 | MessageBox.Show(result.ErrorMessage);
244 | return;
245 | }
246 |
247 | SearchCriteria searchCriteria = GetSearchCriteria();
248 |
249 | // Replace the search controls with the progress control(s)
250 | ShowSearchProgressGrid();
251 |
252 | StatusTextBox.Text = "Loading results. Please be patient.";
253 | Progress.Value = 0;
254 | ResultGrid.Items.Clear();
255 |
256 | // Stop any previous search if it's still working
257 | // (in practice this should not be necessary)
258 | if (_searchManager != null && _searchManager.IsBusy)
259 | {
260 | string msg = "Please wait one moment before searching. ";
261 | MessageBox.Show(msg);
262 | _searchManager.CancelAsync();
263 | }
264 |
265 | // Coordinates the actual scraping/parsing/validation/results:
266 | _searchManager = new SearchManager(searchCriteria,
267 | Constants.MAX_THREADS);
268 | _searchManager.ProgressChanged += ScraperProgressChanged;
269 | _searchManager.RunWorkerCompleted += ScrapeComplete;
270 | _searchManager.RunWorkerAsync();
271 | }
272 |
273 | ///
274 | /// Replaces the search controls with "search progress" controls when a user searches
275 | /// A cancel button and a text area that displays which items were excluded from the results.
276 | ///
277 | private void ShowSearchProgressGrid()
278 | {
279 | SearchLayoutGrid.Visibility = System.Windows.Visibility.Hidden;
280 | SearchProgressGrid.Visibility = System.Windows.Visibility.Visible;
281 |
282 | DataLayoutGrid.ColumnDefinitions[0].Width = new GridLength(84, GridUnitType.Star);
283 | DataLayoutGrid.ColumnDefinitions[1].Width = new GridLength(1, GridUnitType.Star);
284 | DataLayoutGrid.ColumnDefinitions[2].Width = new GridLength(15, GridUnitType.Star);
285 |
286 | // The initial page load can take a few moments, so make the
287 | // progress bar animate to show that work is being done.
288 | SolidColorBrush bg = new SolidColorBrush(Colors.Black);
289 | Progress.Background = bg;
290 |
291 | ColorAnimation animation = new ColorAnimation()
292 | {
293 | From = Colors.Black,
294 | To = Colors.Gray,
295 | Duration = TimeSpan.FromMilliseconds(750),
296 | RepeatBehavior = RepeatBehavior.Forever,
297 | AutoReverse = true,
298 | };
299 |
300 | bg.BeginAnimation(SolidColorBrush.ColorProperty, animation);
301 |
302 | CancelButton.Focus();
303 | }
304 |
305 | ///
306 | /// Makes the search criteria controls visible again after a search is completed (or canceled).
307 | ///
308 | private void ShowSearchCriteriaGrid()
309 | {
310 | DataLayoutGrid.ColumnDefinitions[0].Width = new GridLength(70, GridUnitType.Star);
311 | DataLayoutGrid.ColumnDefinitions[1].Width = new GridLength(1, GridUnitType.Star);
312 | DataLayoutGrid.ColumnDefinitions[2].Width = new GridLength(30, GridUnitType.Star);
313 |
314 | SearchProgressGrid.Visibility = System.Windows.Visibility.Hidden;
315 | SearchLayoutGrid.Visibility = System.Windows.Visibility.Visible;
316 |
317 | // Return focus to the search textbox
318 | txtSearch.Focus();
319 | }
320 |
321 | private void btnSearch_Click(object sender, RoutedEventArgs e)
322 | { Search(); }
323 |
324 | ///
325 | /// Called when the user cancels a search. May take a few
326 | /// seconds while it waits for the async thread to be ready.
327 | ///
328 | private void CancelSearch()
329 | {
330 | if (_searchManager != null)
331 | {
332 | _searchManager.CancelAsync();
333 | }
334 |
335 | // Reset progress bar
336 | Progress.Value = 0;
337 | }
338 |
339 | ///
340 | /// Displays a work progress message in the status textbox.
341 | /// Right now this only displays items that have failed validation. Mostly
342 | /// this is there to show that the application is working and isn't stalled out.
343 | ///
344 | ///
345 | void AppendStatusMessage(string message)
346 | {
347 | if (message == null || message == "") return;
348 |
349 | // Add results to the data grid as soon as they are available
350 | StatusTextBox.Text += Environment.NewLine + Environment.NewLine;
351 | StatusTextBox.Text += message;
352 | StatusTextBox.Focus();
353 | StatusTextBox.CaretIndex = StatusTextBox.Text.Length;
354 | StatusTextBox.ScrollToEnd();
355 |
356 | }
357 |
358 | ///
359 | /// Takes a validated result and adds it to the datagrid
360 | ///
361 | ///
362 | void AddResultToGrid(AmazonItem result)
363 | {
364 | if (result == null) return;
365 | // Add results to the data grid as soon as they are available
366 | try
367 | { ResultGrid.Items.Add(result); }
368 | catch
369 | {
370 | string msg = "Error adding item to the result grid: " +
371 | result.ToString();
372 | Debug.WriteLine(msg);
373 | }
374 | }
375 |
376 | ///
377 | /// Returns the state of the application to "search mode" after a search is complete.
378 | ///
379 | ///
380 | ///
381 | void ScrapeComplete(object sender, RunWorkerCompletedEventArgs e)
382 | {
383 | _searchManager.CancelAsync();
384 | _searchManager.Dispose();
385 |
386 | TaskbarItemInfo.ProgressValue = 1.0;
387 | Progress.Value = 0;
388 |
389 | SolidColorBrush bg = new SolidColorBrush(Colors.Black);
390 | Progress.Background = bg;
391 |
392 | // Show the search controls again
393 | ShowSearchCriteriaGrid();
394 |
395 | }
396 |
397 | ///
398 | /// Called whenever a result comes back. Updates the progress bars.
399 | ///
400 | ///
401 | ///
402 | private void ScraperProgressChanged(object sender, ProgressChangedEventArgs args)
403 | {
404 |
405 | if (args == null || args.UserState == null ||
406 | args.UserState.GetType() != typeof(Result)) return;
407 |
408 | Result result = (Result)args.UserState;
409 |
410 | // Update the status textbox with the result message
411 | AppendStatusMessage(result.StatusMessage);
412 |
413 | // If a new result is found (it passed validation), add it to the grid
414 | if (result.HasReturnValue) AddResultToGrid(result.Value);
415 |
416 | int intPercent = args.ProgressPercentage;
417 | double doublePercent = intPercent / 100.0;
418 |
419 | // Update progress controls
420 | if (intPercent > 0)
421 | {
422 | // Progress bars
423 | TaskbarItemInfo.ProgressValue = doublePercent;
424 | Progress.Value = intPercent;
425 | }
426 | }
427 |
428 |
429 | }
430 | }
--------------------------------------------------------------------------------
/UI/Resources/ControlStyles.xaml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
14 |
15 |
16 |
22 |
23 |
24 |
30 |
31 |
33 |
43 |
44 |
45 |
74 |
75 |
76 |
79 |
80 |
81 |
85 |
86 |
87 |
91 |
92 |
93 |
97 |
98 |
99 |
100 |
114 |
115 |
118 |
119 |
120 |
130 |
131 |
134 |
135 |
138 |
139 |
142 |
143 |
172 |
173 |
174 |
187 |
188 |
--------------------------------------------------------------------------------
/UI/Resources/Images/application_icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ThomasRush/AmazonScrape/0d4702607527968d388cd889e3110179a67f742f/UI/Resources/Images/application_icon.ico
--------------------------------------------------------------------------------
/UI/Resources/Images/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ThomasRush/AmazonScrape/0d4702607527968d388cd889e3110179a67f742f/UI/Resources/Images/icon.ico
--------------------------------------------------------------------------------
/UI/Resources/Images/prime_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ThomasRush/AmazonScrape/0d4702607527968d388cd889e3110179a67f742f/UI/Resources/Images/prime_logo.png
--------------------------------------------------------------------------------
/UI/Resources/ResourceLoader.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.Windows;
4 | using System.Windows.Media.Imaging;
5 |
6 | namespace AmazonScrape
7 | {
8 | ///
9 | /// Loads image or control style resources
10 | ///
11 | public static class ResourceLoader
12 | {
13 |
14 | ///
15 | /// Load the Amazon Prime logo
16 | ///
17 | ///
18 | public static BitmapImage GetPrimeLogoBitmap()
19 | {
20 | BitmapImage primeLogoBitmap = new BitmapImage();
21 | try
22 | {
23 | primeLogoBitmap = new BitmapImage(new Uri("pack://application:,,,/UI/Resources/Images/prime_logo.png", UriKind.RelativeOrAbsolute));
24 | }
25 | catch (Exception)
26 | { Debug.WriteLine("Can't load prime logo resource."); }
27 |
28 | return primeLogoBitmap;
29 | }
30 |
31 | ///
32 | /// Load the program icon
33 | ///
34 | ///
35 | public static BitmapImage GetProgramIconBitmap()
36 | {
37 | BitmapImage programIconBitmap = new BitmapImage();
38 | try
39 | {
40 | programIconBitmap = new BitmapImage(new Uri("pack://application:,,,/UI/Resources/Images/icon.ico", UriKind.RelativeOrAbsolute));
41 | }
42 | catch (Exception)
43 | { Debug.WriteLine("Couldn't load program icon."); }
44 |
45 | return programIconBitmap;
46 | }
47 |
48 | ///
49 | /// Dynamically load a control style.
50 | ///
51 | ///
52 | ///
53 | public static Style GetControlStyle(string styleName)
54 | {
55 | if (!UriParser.IsKnownScheme("pack"))
56 | UriParser.Register(new GenericUriParser(GenericUriParserOptions.GenericAuthority), "pack", -1);
57 |
58 | ResourceDictionary dict = new ResourceDictionary();
59 | Uri uri = new Uri("/UI/Resources/ControlStyles.xaml", UriKind.Relative);
60 | dict.Source = uri;
61 | Application.Current.Resources.MergedDictionaries.Add(dict);
62 |
63 | Style style;
64 | try
65 | {
66 | style = (Style)Application.Current.Resources[styleName];
67 | }
68 | catch
69 | {
70 | throw new ResourceReferenceKeyNotFoundException("Can't find the Style " + styleName, styleName);
71 | }
72 |
73 | return style;
74 | }
75 |
76 |
77 | } // class
78 | } // namespace
79 |
--------------------------------------------------------------------------------
/UtilityClasses/DoubleRange.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 |
4 | namespace AmazonScrape
5 | {
6 | ///
7 | /// Represents a range of double values
8 | ///
9 | [DebuggerDisplay("Range = {Low},{High}")]
10 | [global::System.ComponentModel.TypeConverter(typeof(DoubleRangeConverter))]
11 | public class DoubleRange: NumericRange
12 | {
13 | public double Span { get { return High - Low; } }
14 | public bool HasLow { get { return _low > double.MinValue; } }
15 | public bool HasHigh { get { return _high < double.MaxValue; } }
16 |
17 | // Default constructor sets the range to the min/max
18 | // allowable for a double.
19 | public DoubleRange()
20 | {
21 | _low = double.MinValue;
22 | _high = double.MaxValue;
23 | }
24 |
25 | public DoubleRange(double low, double high) : base(low, high) { }
26 |
27 | // TODO: allow the user to specify one boundary or the other?
28 |
29 | ///
30 | /// Returns true if the supplied double is contained within the range (inclusive)
31 | ///
32 | ///
33 | ///
34 | public override bool Contains(double testDouble)
35 | {
36 | return (testDouble >= Low && testDouble <= High);
37 | }
38 |
39 | ///
40 | /// Returns true if the supplied NumericRange exists entirely within this range (inclusive)
41 | ///
42 | ///
43 | ///
44 | public override bool Contains(NumericRange range)
45 | {
46 | return (Low <= range.Low && High >= range.High);
47 | }
48 |
49 | ///
50 | /// Returns true if there is any overlap between the supplied NumericRange (inclusive)
51 | ///
52 | ///
53 | ///
54 | public override bool Overlaps(NumericRange range)
55 | {
56 | if (Contains(range)) return true;
57 |
58 | if ((range.Low <= Low && range.High >= Low) ||
59 | (range.High >= High && range.Low <= High)) return true;
60 |
61 | return false;
62 | }
63 |
64 | ///
65 | /// Type converter for range values.
66 | /// Range should be in the form of: "X,Y" where X is the numeric low value and Y is the numeric high value
67 | /// If one of the range values is blank, it means that there is no boundary on that end of the range.
68 | /// For example, "X," would mean X is the low value, and the top end of the range is the max value for a double.
69 | /// Note: A comma is required even if no values are being supplied.
70 | ///
71 | ///
72 | ///
73 | public static DoubleRange Parse(string data)
74 | {
75 |
76 | DoubleRange result = new DoubleRange();
77 | if (string.IsNullOrEmpty(data)) return result;
78 |
79 | string[] boundaries = data.Split(',');
80 |
81 | if (boundaries.Length != 2)
82 | {
83 | string msg = "Double Range requires values separated by a comma. Note: you can supply one boundary and leave the other blank, e.g. '0,' means a value zero or greater";
84 | throw new FormatException(msg);
85 | }
86 |
87 | double low = double.MinValue;
88 | double high = double.MaxValue;
89 |
90 | if (boundaries[0].Length > 0)
91 | {
92 | try
93 | {
94 | low = Convert.ToDouble(boundaries[0]);
95 | result.Low = low;
96 | }
97 | catch { throw new FormatException("Can't convert the low boundary in the TextBox control range property. ('" + boundaries[0] + "' supplied)"); }
98 | }
99 |
100 | if (boundaries[1].Length > 0)
101 | {
102 | try
103 | {
104 | high = Convert.ToDouble(boundaries[1]);
105 | result.High = high;
106 | }
107 | catch { throw new FormatException("Can't convert the high boundary in the TextBox control range property. ('" + boundaries[1] + "' supplied)"); }
108 | }
109 |
110 | return result;
111 | }
112 |
113 | public override string ToString()
114 | {
115 | string result = "";
116 |
117 | // Changed format here to support type converter
118 |
119 | if (HasLow)
120 | {
121 | result += Low.ToString();
122 | }
123 |
124 | result += ",";
125 |
126 | if (HasHigh)
127 | {
128 | result += High.ToString();
129 | }
130 |
131 | return result;
132 | }
133 |
134 | } // class
135 |
136 | } // namespace
137 |
--------------------------------------------------------------------------------
/UtilityClasses/DoubleRangeConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ComponentModel;
3 | using System.Globalization;
4 |
5 | namespace AmazonScrape
6 | {
7 | class DoubleRangeConverter : global::System.ComponentModel.TypeConverter
8 | {
9 | // True if source is string
10 | public override bool CanConvertFrom(
11 | System.ComponentModel.ITypeDescriptorContext context, Type sourceType)
12 | {
13 | return sourceType == typeof(string);
14 | }
15 |
16 | // True if DestinationType is DoubleRange
17 | public override bool CanConvertTo(
18 | System.ComponentModel.ITypeDescriptorContext context, Type destinationType)
19 | {
20 | return destinationType == typeof(DoubleRange);
21 |
22 | }
23 |
24 | // Convert string to DoubleRange
25 | public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
26 | {
27 | if (value is string)
28 | {
29 | try
30 | {
31 | return DoubleRange.Parse(value as string);
32 | }
33 | catch (Exception ex)
34 | {
35 | throw new Exception(string.Format(
36 | "Cannot convert '{0}' ({1}) because {2}", value, value.GetType(), ex.Message), ex);
37 | }
38 | }
39 |
40 | return base.ConvertFrom(context, culture, value);
41 | }
42 |
43 |
44 | public override object ConvertTo(
45 | System.ComponentModel.ITypeDescriptorContext context,
46 | System.Globalization.CultureInfo culture, object value, Type destinationType)
47 | {
48 | if (destinationType == null)
49 | throw new ArgumentNullException("destinationType");
50 |
51 | DoubleRange range = value as DoubleRange;
52 |
53 | if (range != null)
54 | if (this.CanConvertTo(context, destinationType))
55 | return range.ToString();
56 |
57 | return base.ConvertTo(context, culture, value, destinationType);
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/UtilityClasses/IntRange.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace AmazonScrape
8 | {
9 | class IntRange : NumericRange
10 | {
11 | public IntRange(int low, int high) : base(low, high) {}
12 |
13 | // The difference between the high and low values
14 | public int Span { get { return _high - _low; } }
15 |
16 | // Default constructor sets the range to the min/max
17 | // allowable for an integer.
18 | public IntRange()
19 | {
20 | this.Low = int.MinValue;
21 | this.High = int.MaxValue;
22 | }
23 |
24 | ///
25 | /// Returns true if the supplied integer falls within this range (inclusive).
26 | ///
27 | ///
28 | ///
29 | public override bool Contains(int testInt)
30 | {
31 | return (testInt >= Low && testInt <= High);
32 | }
33 |
34 | public override bool Contains(NumericRange range)
35 | {
36 | return (Low <= range.Low && High >= range.High);
37 | }
38 |
39 | ///
40 | /// Returns true if this range value overlaps at any point with the supplied range.
41 | ///
42 | ///
43 | ///
44 | public override bool Overlaps(NumericRange range)
45 | {
46 | return (Low <= range.High || High >= range.Low);
47 | }
48 |
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/UtilityClasses/NumericRange.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | //using System.Threading.Tasks;
6 |
7 | namespace AmazonScrape
8 | {
9 | // Represents a numeric range of values.
10 | // Require that the extending classes implement IComparable (numeric types, String, Char, Datetime)
11 | // This gets us some compile-time type-checking (although DateTime, String & Char still cause runtime exceptions)
12 | public abstract class NumericRange where T : IComparable
13 | {
14 | // Allow user to specify range after construction, but ensure that the
15 | // "low" value is less than the "high" value
16 | // (otherwise throws ArgumentOutOfRangeException)
17 | public T Low
18 | {
19 | get
20 | { return _low;}
21 | set
22 | { CheckRange(value, _high); _low = value; HasRangeSpecified = true; }
23 | }
24 |
25 | public T High
26 | {
27 | get
28 | { return _high; }
29 | set
30 | { CheckRange(_low, value); _high = value; HasRangeSpecified = true; }
31 | }
32 |
33 | protected T _low;
34 | protected T _high;
35 |
36 | ///
37 | /// True if the user specifies a range of values; otherwise the range is set to the
38 | /// minimum/maximum of the inheriting numeric data type
39 | ///
40 | public bool HasRangeSpecified { get; protected set; }
41 |
42 |
43 | ///
44 | /// Default constructor
45 | ///
46 | public NumericRange()
47 | {
48 | CheckType();
49 | HasRangeSpecified = false;
50 | }
51 |
52 | ///
53 | /// Constructor allows user to specify the low and high bounds
54 | /// for the numeric range.
55 | ///
56 | ///
57 | ///
58 | public NumericRange(T low, T high)
59 | {
60 | CheckType();
61 |
62 | CheckRange(low, high);
63 | _low = low;
64 | _high = high;
65 | HasRangeSpecified = true;
66 | }
67 |
68 | ///
69 | /// Ensure that the 'low' value does not exceed the 'high' value
70 | ///
71 | /// Note: it is allowable that they are the same value.
72 | ///
73 | ///
74 | private void CheckRange(T low, T high)
75 | {
76 | // Low value can't be greater than high value.
77 | if (low.CompareTo(high) > 0)
78 | {
79 | string msg = "The 'low' value for the range cannot be greater than the 'high' value.";
80 | throw new ArgumentOutOfRangeException(msg);
81 | }
82 | }
83 |
84 | ///
85 | /// Ensure that the implementing class uses a numeric type
86 | /// ( implementing IComparable addresses other types at compile-time )
87 | ///
88 | private void CheckType()
89 | {
90 | Type t = typeof(T);
91 |
92 | // Restrict to numeric data only
93 | // According to MSDN,
94 | // "All numeric types (such as Int32 and Double) implement IComparable,
95 | /// as do String, Char, and DateTime."
96 | // Explicitly check for String, Char, and DateTime :
97 | if (t == typeof(string) ||
98 | t == typeof(DateTime) ||
99 | t == typeof(char))
100 | { throw new NotSupportedException("Only numeric values are supported."); }
101 | }
102 |
103 | public abstract bool Contains(T value); // Generic parameter is contained within the range (inclusive).
104 | public abstract bool Contains(NumericRange testRange); // Range parameter is contained (inclusive)
105 | public abstract bool Overlaps(NumericRange testRange); // Range parameter partially overlaps this range (inclusive)
106 |
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------