DefinedLanguages => [.. LanguageList.OrderBy(x => x.LanguageCode)];
94 | #endregion List of languages
95 | }
96 |
--------------------------------------------------------------------------------
/WUView/Helpers/NLogHelpers.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Tim Kennedy. All Rights Reserved. Licensed under the MIT License.
2 |
3 | namespace WUView.Helpers;
4 |
5 | ///
6 | /// Class for NLog helper methods
7 | ///
8 | internal static class NLogHelpers
9 | {
10 | ///
11 | /// Static instance for NLog Logger.
12 | ///
13 | ///
14 | /// Used with a "static using" in GlobalUsings.cs to avoid creating an instance in every class.
15 | ///
16 | internal static readonly Logger _log = LogManager.GetLogger("logTemp");
17 |
18 | #region Create the NLog configuration
19 | ///
20 | /// Configure NLog
21 | ///
22 | /// True to start with new log file. False to append to current file.
23 | public static void NLogConfig(bool newFile)
24 | {
25 | // Throw exception if there are error in configuration
26 | LogManager.ThrowConfigExceptions = true;
27 |
28 | // New NLog configuration
29 | LoggingConfiguration config = new();
30 |
31 | // create log file Target for NLog
32 | FileTarget logfile = new("logfile")
33 | {
34 | // new file on startup
35 | DeleteOldFileOnStartup = newFile,
36 |
37 | // create the file if needed
38 | FileName = CreateFilename(),
39 |
40 | // message and footer layouts
41 | Footer = "${date:format=yyyy/MM/dd HH\\:mm\\:ss}",
42 | Layout = "${date:format=yyyy/MM/dd HH\\:mm\\:ss} " +
43 | "${pad:padding=-5:inner=${level:uppercase=true}} " +
44 | "${message}${onexception:${newline}${exception:format=tostring}}"
45 | };
46 |
47 | // add the log file target
48 | config.AddTarget(logfile);
49 |
50 | // add the rule for the log file
51 | LoggingRule file = new("*", LogLevel.Debug, logfile)
52 | {
53 | RuleName = "LogToFile"
54 | };
55 | config.LoggingRules.Add(file);
56 |
57 | // create debugger target
58 | DebuggerTarget debugger = new("debugger")
59 | {
60 | Layout = "${processtime} >>> ${message} "
61 | };
62 |
63 | // add the target
64 | config.AddTarget(debugger);
65 |
66 | // add the rule
67 | LoggingRule bug = new("*", LogLevel.Trace, debugger);
68 | config.LoggingRules.Add(bug);
69 |
70 | // add the configuration to NLog
71 | LogManager.Configuration = config;
72 |
73 | // Lastly, set the logging level based on setting
74 | SetLogLevel(UserSettings.Setting!.IncludeDebug);
75 | }
76 | #endregion Create the NLog configuration
77 |
78 | #region Create a filename in the temp folder
79 | private static string CreateFilename()
80 | {
81 | // create filename string
82 | string appName = AppInfo.AppName;
83 | string today = DateTime.Now.ToString("yyyyMMdd", CultureInfo.InvariantCulture);
84 | string filename = Debugger.IsAttached ? $"{appName}.{today}.debug.log" : $"{appName}.{today}.log";
85 |
86 | // combine temp folder with filename
87 | string tempDir = Path.GetTempPath();
88 | return Path.Combine(tempDir, "T_K", filename);
89 | }
90 | #endregion Create a filename in the temp folder
91 |
92 | #region Set NLog logging level
93 | ///
94 | /// Set the NLog logging level to Debug or Info
95 | ///
96 | /// If true set level to Debug, otherwise set to Info
97 | public static void SetLogLevel(bool debug)
98 | {
99 | LoggingConfiguration config = LogManager.Configuration;
100 |
101 | LoggingRule rule = config.FindRuleByName("LogToFile");
102 | if (rule != null)
103 | {
104 | LogLevel level = debug ? LogLevel.Debug : LogLevel.Info;
105 | rule.SetLoggingLevels(level, LogLevel.Fatal);
106 | LogManager.ReconfigExistingLoggers();
107 | }
108 | }
109 | #endregion Set NLog logging level
110 |
111 | #region Get the log file name
112 | ///
113 | /// Gets the filename for the NLog log fie
114 | ///
115 | /// Name of the log file.
116 | public static string? GetLogfileName()
117 | {
118 | LoggingConfiguration config = LogManager.Configuration;
119 | return (config.FindTargetByName("logfile")
120 | as FileTarget)?.FileName.Render(new LogEventInfo { TimeStamp = DateTime.Now });
121 | }
122 | #endregion Get the log file name
123 | }
124 |
--------------------------------------------------------------------------------
/WUView/Styles/ExpanderStyles.xaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
20 |
21 |
22 |
23 |
39 |
40 |
41 |
42 |
58 |
59 |
60 |
61 |
80 |
81 |
82 |
83 |
84 |
104 |
105 |
106 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Windows Update Viewer
8 |
9 |
10 |
11 |
12 | [](https://github.com/Timthreetwelve/WUView/blob/main/LICENSE)
13 | [](https://dotnet.microsoft.com/en-us/download)
14 | [](https://github.com/Timthreetwelve/WUView/releases/latest)
15 | [](https://github.com/Timthreetwelve/WUView/releases/latest)
16 | [](https://github.com/Timthreetwelve/WUView/commits/main)
17 | [](https://github.com/Timthreetwelve/WUView/commits/main)
18 | [](https://github.com/Timthreetwelve/WUView/commits/main)
19 | [](https://docs.github.com/en/get-started/exploring-projects-on-github/saving-repositories-with-stars)
20 | [](https://github.com/Timthreetwelve/WUView/releases)
21 | [](https://github.com/Timthreetwelve/WUView/releases/latest)
22 | [](https://github.com/Timthreetwelve/WUView/issues)
23 | [](https://github.com/Timthreetwelve/WUView/issues)
24 |
25 |
26 |
27 | ### What is Windows Update Viewer?
28 | Windows Update Viewer (WUView) is an application that displays Windows Update history. It is meant to be a lightweight application that is easy to use. There aren't any confusing categories; every update is listed in one place. Updates that you don't want to see can be permanently excluded or temporarily filtered.
29 |
30 | WUView uses the Windows Update Agent API and Windows event logs to display details of installed updates. Event log entries are associated with individual updates by using the "KB" number. If an update does not use a KB number or isn't presented in a consistent format, no event log entries will be displayed.
31 |
32 | Please be aware that Windows Update Viewer can only display updates provided by the [Windows Update Agent API](https://learn.microsoft.com/en-us/windows/win32/wua_sdk/portal-client). If you have reason to suspect that the application isn't returning correct or complete information, see the [Troubleshooting](https://github.com/Timthreetwelve/WUView/wiki/Troubleshooting) and [Known Issues](https://github.com/Timthreetwelve/WUView/wiki/Known-Issues) topics in the Wiki.
33 |
34 | If you are using Windows 10 please see [Known Issues](https://github.com/Timthreetwelve/WUView/wiki/Known-Issues).
35 |
36 | See the [Wiki](https://github.com/Timthreetwelve/WUView/wiki) for additional information.
37 |
38 | ### Windows Update Viewer is multilingual!
39 | Languages are being added as of version 0.5.21. Please see [Contribute a Translation](https://github.com/Timthreetwelve/WUView/wiki/Contribute-a-Translation) topic in the Wiki if you would like to contribute a translation.
40 |
41 | ### Windows Update Viewer uses .NET 8
42 | Self-contained versions are available if .NET 8 isn't installed. See the [releases page](https://github.com/Timthreetwelve/WUView/releases).
43 |
44 | ### Download Windows Update Viewer
45 | You can always download the latest release from the [releases page](https://github.com/Timthreetwelve/WUView/releases). Note that "portable" releases are provided as well as the traditional installers.
46 |
47 | ### Features
48 | * View details for each update, including Event Log entries, if available.
49 | * Easily exclude entries, such as Defender.
50 | * Temporarily filter entries.
51 | * Link to the Support URL.
52 | * Link to HResult explanation (the HResult is placed in the clipboard).
53 | * Toggle the visibility of the details pane.
54 | * Save to a text or CSV file.
55 | * Open Windows Update from the app.
56 | * Choose accent color and one of three themes.
57 | * Adjust app size and row spacing. (Helpful for us users that don't see as well as we used to.)
58 | * Select the interface language.
59 |
60 | ### The Main Window
61 | 
62 |
63 |
64 |
--------------------------------------------------------------------------------
/WUView/Dialogs/MDCustMsgBox.xaml.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Tim Kennedy. All Rights Reserved. Licensed under the MIT License.
2 |
3 | // Inspired by https://stackoverflow.com/a/60302166
4 |
5 | namespace WUView.Dialogs;
6 |
7 | ///
8 | /// Custom message box that works well with Material Design in XAML.
9 | ///
10 | public partial class MDCustMsgBox : Window
11 | {
12 | #region Public Property
13 | public static CustResultType CustResult { get; private set; }
14 | #endregion
15 |
16 | ///
17 | /// Custom message box for MDIX
18 | ///
19 | /// Text of the message
20 | /// Text that goes in the title bar
21 | /// OK, OKCancel, YesNoCancel or YesNo
22 | /// True to hide close button
23 | /// True to make window topmost
24 | /// Owner of the window
25 | /// True will set accent color to red
26 | public MDCustMsgBox(string Message,
27 | string Title,
28 | ButtonType Buttons,
29 | bool HideClose = false,
30 | bool OnTop = true,
31 | Window? MsgBoxOwner = null,
32 | bool IsError = false)
33 | {
34 | InitializeComponent();
35 |
36 | DataContext = this;
37 |
38 | #region Topmost
39 | if (OnTop)
40 | {
41 | Topmost = true;
42 | }
43 | #endregion
44 |
45 | #region Message text
46 | TxtMessage.Text = Message;
47 | #endregion Message text
48 |
49 | #region Message box title
50 | TxtTitle.Text = string.IsNullOrEmpty(Title) ? Application.Current.MainWindow!.Title : Title;
51 | #endregion Message box title
52 |
53 | #region Button visibility
54 | switch (Buttons)
55 | {
56 | case ButtonType.Ok:
57 | BtnCancel.Visibility = Visibility.Collapsed;
58 | BtnYes.Visibility = Visibility.Collapsed;
59 | BtnNo.Visibility = Visibility.Collapsed;
60 | _ = BtnOk.Focus();
61 | break;
62 |
63 | case ButtonType.OkCancel:
64 | BtnYes.Visibility = Visibility.Collapsed;
65 | BtnNo.Visibility = Visibility.Collapsed;
66 | _ = BtnOk.Focus();
67 | break;
68 |
69 | case ButtonType.YesNo:
70 | BtnOk.Visibility = Visibility.Collapsed;
71 | BtnCancel.Visibility = Visibility.Collapsed;
72 | _ = BtnYes.Focus();
73 | break;
74 |
75 | case ButtonType.YesNoCancel:
76 | BtnOk.Visibility = Visibility.Collapsed;
77 | _ = BtnYes.Focus();
78 | break;
79 | }
80 | if (HideClose)
81 | {
82 | BtnClose.Visibility = Visibility.Collapsed;
83 | }
84 | #endregion Button visibility
85 |
86 | #region Window position
87 | if (MsgBoxOwner != null)
88 | {
89 | Owner = MsgBoxOwner;
90 | WindowStartupLocation = Owner.IsVisible ? WindowStartupLocation.CenterOwner : WindowStartupLocation.CenterScreen;
91 | }
92 | else
93 | {
94 | WindowStartupLocation = WindowStartupLocation.CenterScreen;
95 | }
96 | #endregion Window position
97 |
98 | #region Error message
99 | if (IsError)
100 | {
101 | BorderBrush = Brushes.OrangeRed;
102 | BorderThickness = new Thickness(2);
103 | CardHeader.Background = BorderBrush;
104 | CardHeader.FontWeight = FontWeights.Bold;
105 | }
106 | #endregion Error message
107 | }
108 |
109 | #region Mouse event
110 | private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
111 | {
112 | DragMove();
113 | }
114 | #endregion Mouse event
115 |
116 | #region Button commands
117 | [RelayCommand]
118 | private void CancelButton()
119 | {
120 | Close();
121 | CustResult = CustResultType.Cancel;
122 | }
123 |
124 | [RelayCommand]
125 | private void OKButton()
126 | {
127 | Close();
128 | CustResult = CustResultType.Ok;
129 | }
130 |
131 | [RelayCommand]
132 | private void YesButton()
133 | {
134 | Close();
135 | CustResult = CustResultType.Yes;
136 | }
137 |
138 | [RelayCommand]
139 | private void NoButton()
140 | {
141 | Close();
142 | CustResult = CustResultType.No;
143 | }
144 | #endregion Button commands
145 | }
146 |
147 | #region Button type enumeration
148 | public enum ButtonType
149 | {
150 | OkCancel,
151 | YesNo,
152 | YesNoCancel,
153 | Ok,
154 | }
155 | #endregion Button type enumeration
156 |
157 | #region Result type enumeration
158 | public enum CustResultType
159 | {
160 | Ok,
161 | Yes,
162 | No,
163 | Cancel
164 | }
165 | #endregion Result type enumeration
166 |
--------------------------------------------------------------------------------
/WUView/Helpers/GitHubHelpers.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Tim Kennedy. All Rights Reserved. Licensed under the MIT License.
2 |
3 | using Octokit;
4 |
5 | namespace WUView.Helpers;
6 |
7 | ///
8 | /// Class for methods that check GitHub for releases
9 | ///
10 | internal static class GitHubHelpers
11 | {
12 | #region MainWindow Instance
13 | private static readonly MainWindow? _mainWindow = System.Windows.Application.Current.MainWindow as MainWindow;
14 | #endregion MainWindow Instance
15 |
16 | #region Check for newer release
17 | ///
18 | /// Checks to see if a newer release is available.
19 | ///
20 | ///
21 | /// If the release version is greater than the current version
22 | /// a message box will be shown asking to go to the releases page.
23 | ///
24 | public static async Task CheckRelease()
25 | {
26 | try
27 | {
28 | SnackbarMsg.ClearAndQueueMessage(GetStringResource("MsgText_AppUpdateChecking"));
29 | Release? release = await GetLatestReleaseAsync(AppConstString.RepoOwner, AppConstString.RepoName);
30 | if (release == null)
31 | {
32 | CheckFailed();
33 | return;
34 | }
35 |
36 | string tag = release.TagName;
37 | if (string.IsNullOrEmpty(tag))
38 | {
39 | CheckFailed();
40 | return;
41 | }
42 |
43 | if (tag.StartsWith("v", StringComparison.InvariantCultureIgnoreCase))
44 | {
45 | tag = tag.ToLower(CultureInfo.InvariantCulture).TrimStart('v');
46 | }
47 |
48 | Version latestVersion = new(tag);
49 |
50 | _log.Debug($"Latest version is {latestVersion} released on {release.PublishedAt!.Value.UtcDateTime} UTC");
51 |
52 | if (latestVersion <= AppInfo.AppVersionVer)
53 | {
54 | string msg = GetStringResource("MsgText_AppUpdateNoneFound");
55 | _log.Debug(msg);
56 | _ = new MDCustMsgBox(msg,
57 | "Windows Update Viewer",
58 | ButtonType.Ok,
59 | false,
60 | true,
61 | _mainWindow).ShowDialog();
62 | }
63 | else
64 | {
65 | _log.Debug($"A newer release ({latestVersion}) has been found.");
66 | string msg = string.Format(CultureInfo.InvariantCulture, MsgTextAppUpdateNewerFound, latestVersion);
67 | _ = new MDCustMsgBox($"{msg}\n\n" +
68 | $"{GetStringResource("MsgText_AppUpdateGoToRelease")}\n\n" +
69 | $"{GetStringResource("MsgText_AppUpdateClose")}",
70 | "Windows Update Viewer",
71 | ButtonType.YesNo,
72 | false,
73 | true,
74 | _mainWindow).ShowDialog();
75 |
76 | if (MDCustMsgBox.CustResult == CustResultType.Yes)
77 | {
78 | string opening = GetStringResource("MsgText_Opening");
79 | _log.Debug($"{opening} {release.HtmlUrl}");
80 | string url = release.HtmlUrl;
81 | Process p = new();
82 | p.StartInfo.FileName = url;
83 | p.StartInfo.UseShellExecute = true;
84 | p.Start();
85 | System.Windows.Application.Current.Shutdown();
86 | }
87 | }
88 | }
89 | catch (Exception ex)
90 | {
91 | _log.Error(ex, "Error encountered while checking version");
92 | CheckFailed();
93 | }
94 | }
95 | #endregion Check for newer release
96 |
97 | #region Get latest release
98 | ///
99 | /// Gets the latest release.
100 | ///
101 | /// The repository owner.
102 | /// Name of the repository.
103 | /// Release object
104 | private static async Task GetLatestReleaseAsync(string repoOwner, string repoName)
105 | {
106 | try
107 | {
108 | GitHubClient client = new(new ProductHeaderValue(repoName));
109 | _log.Debug("Checking GitHub for latest release.");
110 |
111 | return await client.Repository.Release.GetLatest(repoOwner, repoName);
112 | }
113 | catch (Exception ex)
114 | {
115 | _log.Error(ex, "Get latest release from GitHub failed.");
116 | return null!;
117 | }
118 | }
119 | #endregion Get latest release
120 |
121 | #region Check failed message
122 | ///
123 | /// Display a message box stating that the release check failed.
124 | ///
125 | private static void CheckFailed()
126 | {
127 | _ = new MDCustMsgBox(GetStringResource("MsgText_AppUpdateCheckFailed"),
128 | "Windows Update Viewer",
129 | ButtonType.Ok,
130 | false,
131 | true,
132 | _mainWindow,
133 | true).ShowDialog();
134 | }
135 | #endregion Check failed message
136 | }
137 |
--------------------------------------------------------------------------------
/WUView/Models/Enums.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Tim Kennedy. All Rights Reserved. Licensed under the MIT License.
2 |
3 | namespace WUView.Models;
4 |
5 | ///
6 | /// Navigation Page
7 | ///
8 | public enum NavPage
9 | {
10 | Viewer = 0,
11 | Settings = 1,
12 | About = 2,
13 | Exit = 3
14 | }
15 |
16 | ///
17 | /// Theme type, Light, Dark, or System
18 | ///
19 | [TypeConverter(typeof(EnumDescriptionTypeConverter))]
20 | public enum ThemeType
21 | {
22 | [LocalizedDescription("SettingsEnum_Theme_Light")]
23 | Light = 0,
24 | [LocalizedDescription("SettingsEnum_Theme_Dark")]
25 | Dark = 1,
26 | [LocalizedDescription("SettingsEnum_Theme_Darker")]
27 | Darker = 2,
28 | [LocalizedDescription("SettingsEnum_Theme_System")]
29 | System = 3,
30 | [LocalizedDescription("SettingsEnum_Theme_DarkBlue")]
31 | DarkBlue = 4
32 | }
33 |
34 | ///
35 | /// Size of the UI, Smallest, Smaller, Small, Default, Large, Larger, or Largest
36 | ///
37 | [TypeConverter(typeof(EnumDescriptionTypeConverter))]
38 | public enum MySize
39 | {
40 | [LocalizedDescription("SettingsEnum_Size_Smallest")]
41 | Smallest = 0,
42 | [LocalizedDescription("SettingsEnum_Size_Smaller")]
43 | Smaller = 1,
44 | [LocalizedDescription("SettingsEnum_Size_Small")]
45 | Small = 2,
46 | [LocalizedDescription("SettingsEnum_Size_Default")]
47 | Default = 3,
48 | [LocalizedDescription("SettingsEnum_Size_Large")]
49 | Large = 4,
50 | [LocalizedDescription("SettingsEnum_Size_Larger")]
51 | Larger = 5,
52 | [LocalizedDescription("SettingsEnum_Size_Largest")]
53 | Largest = 6
54 | }
55 |
56 | ///
57 | /// One of the 19 predefined Material Design in XAML colors
58 | ///
59 | [TypeConverter(typeof(EnumDescriptionTypeConverter))]
60 | public enum AccentColor
61 | {
62 | [LocalizedDescription("SettingsEnum_AccentColor_Red")]
63 | Red = 0,
64 | [LocalizedDescription("SettingsEnum_AccentColor_Pink")]
65 | Pink = 1,
66 | [LocalizedDescription("SettingsEnum_AccentColor_Purple")]
67 | Purple = 2,
68 | [LocalizedDescription("SettingsEnum_AccentColor_DeepPurple")]
69 | DeepPurple = 3,
70 | [LocalizedDescription("SettingsEnum_AccentColor_Indigo")]
71 | Indigo = 4,
72 | [LocalizedDescription("SettingsEnum_AccentColor_Blue")]
73 | Blue = 5,
74 | [LocalizedDescription("SettingsEnum_AccentColor_LightBlue")]
75 | LightBlue = 6,
76 | [LocalizedDescription("SettingsEnum_AccentColor_Cyan")]
77 | Cyan = 7,
78 | [LocalizedDescription("SettingsEnum_AccentColor_Teal")]
79 | Teal = 8,
80 | [LocalizedDescription("SettingsEnum_AccentColor_Green")]
81 | Green = 9,
82 | [LocalizedDescription("SettingsEnum_AccentColor_LightGreen")]
83 | LightGreen = 10,
84 | [LocalizedDescription("SettingsEnum_AccentColor_Lime")]
85 | Lime = 11,
86 | [LocalizedDescription("SettingsEnum_AccentColor_Yellow")]
87 | Yellow = 12,
88 | [LocalizedDescription("SettingsEnum_AccentColor_Amber")]
89 | Amber = 13,
90 | [LocalizedDescription("SettingsEnum_AccentColor_Orange")]
91 | Orange = 14,
92 | [LocalizedDescription("SettingsEnum_AccentColor_DeepOrange")]
93 | DeepOrange = 15,
94 | [LocalizedDescription("SettingsEnum_AccentColor_Brown")]
95 | Brown = 16,
96 | [LocalizedDescription("SettingsEnum_AccentColor_Gray")]
97 | Gray = 17,
98 | [LocalizedDescription("SettingsEnum_AccentColor_BlueGray")]
99 | BlueGray = 18,
100 | [LocalizedDescription("SettingsEnum_AccentColor_Black")]
101 | Black = 19,
102 | [LocalizedDescription("SettingsEnum_AccentColor_White")]
103 | White = 20,
104 | }
105 |
106 | ///
107 | /// Space between rows in the DataGrid
108 | ///
109 | [TypeConverter(typeof(EnumDescriptionTypeConverter))]
110 | public enum Spacing
111 | {
112 | [LocalizedDescription("SettingsEnum_Spacing_Compact")]
113 | Compact = 0,
114 | [LocalizedDescription("SettingsEnum_Spacing_Comfortable")]
115 | Comfortable = 1,
116 | [LocalizedDescription("SettingsEnum_Spacing_Wide")]
117 | Wide = 2
118 | }
119 |
120 | ///
121 | /// Maximum number of updates to get
122 | ///
123 | [TypeConverter(typeof(EnumDescriptionTypeConverter))]
124 | public enum MaxUpdates
125 | {
126 | [LocalizedDescription("SettingsEnum_MaxUpdates_All")]
127 | All = 0,
128 | [LocalizedDescription("SettingsEnum_MaxUpdates_50")]
129 | Max50 = 1,
130 | [LocalizedDescription("SettingsEnum_MaxUpdates_100")]
131 | Max100 = 2,
132 | [LocalizedDescription("SettingsEnum_MaxUpdates_250")]
133 | Max250 = 3,
134 | [LocalizedDescription("SettingsEnum_MaxUpdates_500")]
135 | Max500 = 4
136 | }
137 |
138 | ///
139 | /// Date format choices
140 | ///
141 | ///
142 | [TypeConverter(typeof(EnumDescriptionTypeConverter))]
143 | public enum DateFormat
144 | {
145 | [Description("MM/dd/yyyy")]
146 | MMddyyyy = 0,
147 | [Description("yyyy/MM/dd HH:mm")]
148 | yyyyMMddHHmm = 1,
149 | [Description("MM/dd/yyyy hh:mm tt")]
150 | MMddyyyhhmmtt = 2,
151 | [Description("d-MMM-yyyy H:mm")]
152 | dMMMyyyyHmm = 3,
153 | [Description("yyyy-MM-dd HH:mm UTC")]
154 | yyyyMMddHHmmUTC = 4,
155 | [Description("dd/MM/yyyy")]
156 | ddMMyyyy = 5,
157 | [Description("dd/MM/yyyy HH:mm")]
158 | ddMMyyyyHHmm = 6,
159 | [Description("yyyy/MM/dd")]
160 | ddMMyy = 7,
161 | [LocalizedDescription("SettingsEnum_DateFormat_RegionalDate")]
162 | RegionalDateOnly = 8,
163 | [LocalizedDescription("SettingsEnum_DateFormat_RegionalDateTime")]
164 | RegionalDateTime = 9
165 | }
166 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | timthreetwelve@outlook.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/WUView/Helpers/AppInfo.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Tim Kennedy. All Rights Reserved. Licensed under the MIT License.
2 |
3 | namespace WUView.Helpers;
4 |
5 | ///
6 | /// Class to return information about the current application
7 | ///
8 | public static class AppInfo
9 | {
10 | ///
11 | /// Returns the operating system description e.g. Microsoft Windows 10.0.19044
12 | ///
13 | public static string OsPlatform => RuntimeInformation.OSDescription;
14 |
15 | ///
16 | /// Returns the framework name
17 | ///
18 | public static string? Framework => Assembly.GetEntryAssembly()?.GetCustomAttribute()?.FrameworkName;
19 |
20 | ///
21 | /// Returns the framework description
22 | ///
23 | public static string RuntimeVersion => RuntimeInformation.FrameworkDescription;
24 |
25 | ///
26 | /// Returns the version number in Major.Minor.Build format
27 | ///
28 | private static string TitleVersion => Assembly.GetEntryAssembly()!.GetName().Version!.ToString().Remove(Assembly.GetEntryAssembly()!.GetName().Version!.ToString().LastIndexOf('.'));
29 |
30 | ///
31 | /// Returns the file version
32 | ///
33 | public static string AppFileVersion => (Assembly.GetEntryAssembly()!.GetCustomAttribute()?.Version) ?? "missing";
34 |
35 | ///
36 | /// Returns the full version number as String
37 | ///
38 | public static string AppVersion => Assembly.GetEntryAssembly()!.GetName().Version!.ToString();
39 |
40 | ///
41 | /// Returns the full version number as Version
42 | ///
43 | public static Version AppVersionVer => Assembly.GetEntryAssembly()!.GetName().Version!;
44 |
45 | ///
46 | /// Returns the app's full path including the EXE name
47 | ///
48 | public static string AppPath => Environment.ProcessPath!;
49 |
50 | ///
51 | /// Returns the app's full path excluding the EXE name
52 | ///
53 | public static string AppDirectory => Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location) ?? "missing";
54 |
55 | ///
56 | /// Returns the app's name without the extension
57 | ///
58 | public static string AppName => (Assembly.GetEntryAssembly()!.GetName().Name) ?? "missing";
59 |
60 | ///
61 | /// Returns the app's name with the extension
62 | ///
63 | public static string AppExeName => Path.GetFileName(AppPath);
64 |
65 | ///
66 | /// Returns the app's full name (name, version, culture, etc.)
67 | ///
68 | public static string AppFullName => Assembly.GetEntryAssembly()!.GetName().FullName;
69 |
70 | ///
71 | /// Returns the Company Name from the Assembly info
72 | ///
73 | public static string AppCompany => FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly()!.Location).CompanyName ?? "missing";
74 |
75 | ///
76 | /// Returns the Author from the Assembly info
77 | ///
78 | public static string AppDescription => FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly()!.Location).FileDescription ?? "missing";
79 |
80 | ///
81 | /// Returns the product version from the Assembly info
82 | ///
83 | public static string AppProductVersion => FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly()!.Location).ProductVersion ?? "missing";
84 |
85 | ///
86 | /// Returns the Copyright info from the Assembly info
87 | ///
88 | public static string AppCopyright => FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly()!.Location).LegalCopyright ?? "missing";
89 |
90 | ///
91 | /// Returns the Product Name from the Assembly info
92 | ///
93 | public static string AppProduct => FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly()!.Location).ProductName ?? "missing";
94 |
95 | ///
96 | /// Returns the File Name from the Assembly info
97 | ///
98 | public static string AppFileName => FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly()!.Location).FileName;
99 |
100 | ///
101 | /// Combines the product name with the title version.
102 | ///
103 | ///
104 | /// String in the format: AppName - 0.0.1
105 | ///
106 | public static string ToolTipVersion => $"{AppProduct} - {TitleVersion}";
107 |
108 | ///
109 | /// Returns the Process Name
110 | ///
111 | public static string AppProcessName => Process.GetCurrentProcess().ProcessName;
112 |
113 | ///
114 | /// Returns the Process ID as Int
115 | ///
116 | public static int AppProcessID => Environment.ProcessId;
117 |
118 | ///
119 | /// Returns the Process Start Time as DateTime
120 | ///
121 | public static DateTime AppProcessStart => Process.GetCurrentProcess().StartTime;
122 |
123 | ///
124 | /// Returns the Process MainModule
125 | ///
126 | public static string AppProcessMainModule => Process.GetCurrentProcess().MainModule!.ModuleName;
127 |
128 | ///
129 | /// The CLR version
130 | ///
131 | public static string CLRVersion => Environment.Version.ToString();
132 |
133 | ///
134 | /// True if running as administrator
135 | ///
136 | public static bool IsAdmin => new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
137 | }
138 |
--------------------------------------------------------------------------------
/WUView/Dialogs/ExcludesEditor.xaml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
22 |
23 |
24 |
25 |
26 |
31 |
32 |
33 |
34 |
35 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
58 |
59 |
60 |
61 |
62 |
63 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
89 |
90 |
98 |
99 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/WUView/Configuration/UserSettings.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Tim Kennedy. All Rights Reserved. Licensed under the MIT License.
2 |
3 | namespace WUView.Configuration;
4 |
5 | [INotifyPropertyChanged]
6 | public partial class UserSettings : ConfigManager
7 | {
8 | #region Properties
9 | ///
10 | /// Select the first row of the data grid.
11 | ///
12 | [ObservableProperty]
13 | private bool _autoSelectFirstRow;
14 |
15 | ///
16 | /// Show updates with the current date in bold.
17 | ///
18 | [ObservableProperty]
19 | private static bool _boldToday = true;
20 |
21 | ///
22 | /// Used to persist the column order if the user changes it.
23 | ///
24 | [ObservableProperty]
25 | private int _columnDate = 1;
26 |
27 | ///
28 | /// Used to persist the column order if the user changes it.
29 | ///
30 | [ObservableProperty]
31 | private static int _columnKB;
32 |
33 | ///
34 | /// Used to persist the column order if the user changes it.
35 | ///
36 | [ObservableProperty]
37 | private int _columnResult = 3;
38 |
39 | ///
40 | /// Used to persist the column order if the user changes it.
41 | ///
42 | [ObservableProperty]
43 | private int _columnTitle = 2;
44 |
45 | ///
46 | /// Date format.
47 | ///
48 | [ObservableProperty]
49 | private int _dateFormat = 9;
50 |
51 | ///
52 | /// Height of the details pane.
53 | ///
54 | [ObservableProperty]
55 | private double _detailsHeight = 300;
56 |
57 | ///
58 | /// Used to determine used to determine scaling of dialogs.
59 | ///
60 | [ObservableProperty]
61 | private static double _dialogScale = 1;
62 |
63 | ///
64 | /// Toggle inclusion of KB number and result when excluding updates.
65 | ///
66 | [ObservableProperty]
67 | private bool _excludeKBandResult;
68 |
69 | ///
70 | /// Toggle hiding excluded updates.
71 | ///
72 | [ObservableProperty]
73 | private bool _hideExcluded = true;
74 |
75 | ///
76 | /// Include debug level messages in the log file.
77 | ///
78 | [ObservableProperty]
79 | private bool _includeDebug = true;
80 |
81 | ///
82 | /// Keep window topmost.
83 | ///
84 | [ObservableProperty]
85 | private bool _keepOnTop;
86 |
87 | ///
88 | /// Enable language testing.
89 | ///
90 | [ObservableProperty]
91 | private bool _languageTesting;
92 |
93 | ///
94 | /// Maximum number of updates to get.
95 | ///
96 | [ObservableProperty]
97 | private MaxUpdates _maxUpdates = MaxUpdates.All;
98 |
99 | ///
100 | /// Accent color.
101 | ///
102 | [ObservableProperty]
103 | private AccentColor _primaryColor = AccentColor.Blue;
104 |
105 | ///
106 | /// Vertical spacing in the data grids.
107 | ///
108 | [ObservableProperty]
109 | private Spacing _rowSpacing = Spacing.Comfortable;
110 |
111 | ///
112 | /// Font used in datagrids.
113 | ///
114 | [ObservableProperty]
115 | private string? _selectedFont = "Segoe UI";
116 |
117 | ///
118 | /// Font size used throughout the application.
119 | /// Defaults to 14 which was the original size.
120 | ///
121 | [ObservableProperty]
122 | private double _selectedFontSize = 14;
123 |
124 | ///
125 | /// Show the details pane at the bottom.
126 | ///
127 | [ObservableProperty]
128 | private bool _showDetails = true;
129 |
130 | ///
131 | /// Show Exit in the navigation menu.
132 | ///
133 | [ObservableProperty]
134 | private bool _showExitInNav = true;
135 |
136 | ///
137 | /// Show messages in log for updates with non-zero result code.
138 | ///
139 | [ObservableProperty]
140 | private bool _showLogWarnings = true;
141 |
142 | ///
143 | /// Option start with window centered on screen.
144 | ///
145 | [ObservableProperty]
146 | private bool _startCentered = true;
147 |
148 | ///
149 | /// Defined language to use in the UI.
150 | ///
151 | [ObservableProperty]
152 | private string _uILanguage = "en-US";
153 |
154 | ///
155 | /// Amount of UI zoom.
156 | ///
157 | [ObservableProperty]
158 | private MySize _uISize = MySize.Default;
159 |
160 | ///
161 | /// Theme type.
162 | ///
163 | [ObservableProperty]
164 | private ThemeType _uITheme = ThemeType.System;
165 |
166 | ///
167 | /// Use accent color for snack bar message background.
168 | ///
169 | [ObservableProperty]
170 | private bool _useAccentColorOnSnackbar;
171 |
172 | ///
173 | /// Use the operating system language (if one has been provided).
174 | ///
175 | [ObservableProperty]
176 | private bool _useOSLanguage = true;
177 |
178 | ///
179 | /// Height of the window.
180 | ///
181 | [ObservableProperty]
182 | private double _windowHeight = 650;
183 |
184 | ///
185 | /// Position of left side of the window.
186 | ///
187 | [ObservableProperty]
188 | private double _windowLeft = 100;
189 |
190 | ///
191 | /// Position of the top side of the window.
192 | ///
193 | [ObservableProperty]
194 | private double _windowTop = 100;
195 |
196 | ///
197 | /// Width of the window.
198 | ///
199 | [ObservableProperty]
200 | private double _windowWidth = 1200;
201 | #endregion Properties
202 | }
203 |
--------------------------------------------------------------------------------
/WUView/Configuration/ConfigHelpers.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Tim Kennedy. All Rights Reserved. Licensed under the MIT License.
2 |
3 | namespace WUView.Configuration;
4 |
5 | ///
6 | /// Class for methods used for creating, reading and saving settings.
7 | ///
8 | public static class ConfigHelpers
9 | {
10 | #region Properties
11 | public static string SettingsFileName { get; private set; } = null!;
12 |
13 | private static JsonSerializerOptions JsonOptions { get; } = new()
14 | {
15 | WriteIndented = true
16 | };
17 | #endregion Properties
18 |
19 | #region MainWindow Instance
20 | private static readonly MainWindow? _mainWindow = Application.Current.MainWindow as MainWindow;
21 | #endregion MainWindow Instance
22 |
23 | #region Initialize settings
24 | ///
25 | /// Initialization method. Gets the file name for settings file and creates it if it
26 | /// doesn't exist.
27 | ///
28 | /// Option name of settings file
29 | public static void InitializeSettings(string settingsFile = "usersettings.json")
30 | {
31 | string? settingsDir = Path.GetDirectoryName(AppContext.BaseDirectory);
32 | SettingsFileName = Path.Combine(settingsDir!, settingsFile);
33 |
34 | if (!File.Exists(SettingsFileName))
35 | {
36 | UserSettings.Setting = new UserSettings();
37 | SaveSettings();
38 | }
39 | ConfigManager.Setting = ReadConfiguration();
40 |
41 | ConfigManager.Setting = new TempSettings();
42 | }
43 | #endregion Initialize settings
44 |
45 | #region Read setting from file
46 | ///
47 | /// Read settings from JSON file.
48 | ///
49 | /// UserSettings
50 | private static UserSettings ReadConfiguration()
51 | {
52 | try
53 | {
54 | string json = File.ReadAllText(SettingsFileName);
55 | UserSettings? settings = JsonSerializer.Deserialize(json);
56 | return settings!;
57 | }
58 | catch (Exception ex)
59 | {
60 | _ = MessageBox.Show($"Error reading settings file.\n{ex.Message}",
61 | "Error",
62 | MessageBoxButton.OK,
63 | MessageBoxImage.Error);
64 | return new UserSettings();
65 | }
66 | }
67 | #endregion Read setting from file
68 |
69 | #region Save settings to JSON file
70 | ///
71 | /// Write settings to JSON file.
72 | ///
73 | public static void SaveSettings()
74 | {
75 | try
76 | {
77 | string json = JsonSerializer.Serialize(UserSettings.Setting, JsonOptions);
78 | File.WriteAllText(SettingsFileName, json);
79 | }
80 | catch (Exception ex)
81 | {
82 | _ = MessageBox.Show($"{GetStringResource("MsgText_ErrorSavingSettings")}\n{ex.Message}",
83 | GetStringResource("MsgText_ErrorCaption"),
84 | MessageBoxButton.OK,
85 | MessageBoxImage.Error);
86 | }
87 | }
88 | #endregion Save settings to JSON file
89 |
90 | #region Export settings
91 | ///
92 | /// Exports the current settings to a JSON file.
93 | ///
94 | public static void ExportSettings()
95 | {
96 | try
97 | {
98 | string appPart = AppInfo.AppProduct.Replace(" ", "");
99 | string settingsPart = GetStringResource("NavItem_Settings");
100 | string datePart = DateTime.Now.ToString("yyyyMMdd", CultureInfo.CurrentCulture);
101 | SaveFileDialog saveFile = new()
102 | {
103 | CheckPathExists = true,
104 | Filter = "JSON File|*.json|All Files|*.*",
105 | FileName = $"{appPart}_{settingsPart}_{datePart}.json"
106 | };
107 |
108 | if (saveFile.ShowDialog() == true)
109 | {
110 | _log.Debug($"Exporting settings file to {saveFile.FileName}.");
111 | string json = JsonSerializer.Serialize(UserSettings.Setting, JsonOptions);
112 | File.WriteAllText(saveFile.FileName, json);
113 | }
114 | }
115 | catch (Exception ex)
116 | {
117 | _log.Debug(ex, "Error exporting settings file.");
118 | _ = MessageBox.Show($"{GetStringResource("MsgText_ErrorExportingSettings")}\n{ex.Message}",
119 | GetStringResource("MsgText_ErrorCaption"),
120 | MessageBoxButton.OK,
121 | MessageBoxImage.Error);
122 | }
123 | }
124 | #endregion Export settings
125 |
126 | #region Import settings
127 | ///
128 | /// Imports settings from a previously exported file.
129 | ///
130 | public static void ImportSettings()
131 | {
132 | try
133 | {
134 | OpenFileDialog importFile = new()
135 | {
136 | CheckPathExists = true,
137 | CheckFileExists = true,
138 | Filter = "JSON File|*.json",
139 | InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop)
140 | };
141 |
142 | if (importFile.ShowDialog() == true)
143 | {
144 | _log.Debug($"Importing settings file from {importFile.FileName}.");
145 | ConfigManager.Setting = JsonSerializer.Deserialize(File.ReadAllText(importFile.FileName))!;
146 | SaveSettings();
147 |
148 | _ = new MDCustMsgBox($"{GetStringResource("MsgText_ImportSettingsRestart")}",
149 | "Windows Update Viewer",
150 | ButtonType.Ok,
151 | false,
152 | true,
153 | _mainWindow).ShowDialog();
154 | }
155 | }
156 | catch (Exception ex)
157 | {
158 | _log.Debug(ex, "Error importing settings file.");
159 | _ = MessageBox.Show($"{GetStringResource("MsgText_ErrorImportingSettings")}\n{ex.Message}",
160 | GetStringResource("MsgText_ErrorCaption"),
161 | MessageBoxButton.OK,
162 | MessageBoxImage.Error);
163 | }
164 | }
165 | #endregion Import settings
166 |
167 | #region Dump settings into the log
168 | ///
169 | /// Dumps (writes) current settings to the log file.
170 | ///
171 | public static void DumpSettings()
172 | {
173 | string dashes = new('-', 25);
174 | string header = $"{dashes} Begin Settings {dashes}";
175 | string trailer = $"{dashes} End Settings {dashes}";
176 | _log.Debug(header);
177 | PropertyInfo[] properties = typeof(UserSettings).GetProperties();
178 | int maxLength = properties.Max(s => s.Name.Length);
179 | foreach (PropertyInfo property in properties)
180 | {
181 | string? value = property.GetValue(UserSettings.Setting, [])!.ToString();
182 | _log.Debug($"{property.Name.PadRight(maxLength)} : {value}");
183 | }
184 | _log.Debug(trailer);
185 | }
186 | #endregion Dump settings into the log
187 | }
188 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | fileeditor
7 |
8 | # User-specific files
9 | *.rsuser
10 | *.suo
11 | *.user
12 | *.userosscache
13 | *.sln.docstates
14 |
15 | # User-specific files (MonoDevelop/Xamarin Studio)
16 | *.userprefs
17 |
18 | # Build results
19 | [Dd]ebug/
20 | [Dd]ebugPublic/
21 | [Rr]elease/
22 | [Rr]eleases/
23 | x64/
24 | x86/
25 | [Aa][Rr][Mm]/
26 | [Aa][Rr][Mm]64/
27 | bld/
28 | [Bb]in/
29 | [Oo]bj/
30 | [Ll]og/
31 |
32 | # Visual Studio 2015/2017 cache/options directory
33 | .vs/
34 | # Uncomment if you have tasks that create the project's static files in wwwroot
35 | #wwwroot/
36 |
37 | # Visual Studio 2017 auto generated files
38 | Generated\ Files/
39 |
40 | # MSTest test Results
41 | [Tt]est[Rr]esult*/
42 | [Bb]uild[Ll]og.*
43 |
44 | # NUNIT
45 | *.VisualState.xml
46 | TestResult.xml
47 |
48 | # Build Results of an ATL Project
49 | [Dd]ebugPS/
50 | [Rr]eleasePS/
51 | dlldata.c
52 |
53 | # Benchmark Results
54 | BenchmarkDotNet.Artifacts/
55 |
56 | # .NET Core
57 | project.lock.json
58 | project.fragment.lock.json
59 | artifacts/
60 |
61 | # StyleCop
62 | StyleCopReport.xml
63 |
64 | # Files built by Visual Studio
65 | *_i.c
66 | *_p.c
67 | *_h.h
68 | *.ilk
69 | *.meta
70 | *.obj
71 | *.iobj
72 | *.pch
73 | *.pdb
74 | *.ipdb
75 | *.pgc
76 | *.pgd
77 | *.rsp
78 | *.sbr
79 | *.tlb
80 | *.tli
81 | *.tlh
82 | *.tmp
83 | *.tmp_proj
84 | *_wpftmp.csproj
85 | *.log
86 | *.vspscc
87 | *.vssscc
88 | .builds
89 | *.pidb
90 | *.svclog
91 | *.scc
92 |
93 | # Chutzpah Test files
94 | _Chutzpah*
95 |
96 | # Visual C++ cache files
97 | ipch/
98 | *.aps
99 | *.ncb
100 | *.opendb
101 | *.opensdf
102 | *.sdf
103 | *.cachefile
104 | *.VC.db
105 | *.VC.VC.opendb
106 |
107 | # Visual Studio profiler
108 | *.psess
109 | *.vsp
110 | *.vspx
111 | *.sap
112 |
113 | # Visual Studio Trace Files
114 | *.e2e
115 |
116 | # TFS 2012 Local Workspace
117 | $tf/
118 |
119 | # Guidance Automation Toolkit
120 | *.gpState
121 |
122 | # ReSharper is a .NET coding add-in
123 | _ReSharper*/
124 | *.[Rr]e[Ss]harper
125 | *.DotSettings.user
126 |
127 | # JustCode is a .NET coding add-in
128 | .JustCode
129 |
130 | # TeamCity is a build add-in
131 | _TeamCity*
132 |
133 | # DotCover is a Code Coverage Tool
134 | *.dotCover
135 |
136 | # AxoCover is a Code Coverage Tool
137 | .axoCover/*
138 | !.axoCover/settings.json
139 |
140 | # Visual Studio code coverage results
141 | *.coverage
142 | *.coveragexml
143 |
144 | # NCrunch
145 | _NCrunch_*
146 | .*crunch*.local.xml
147 | nCrunchTemp_*
148 |
149 | # MightyMoose
150 | *.mm.*
151 | AutoTest.Net/
152 |
153 | # Web workbench (sass)
154 | .sass-cache/
155 |
156 | # Installshield output folder
157 | [Ee]xpress/
158 |
159 | # DocProject is a documentation generator add-in
160 | DocProject/buildhelp/
161 | DocProject/Help/*.HxT
162 | DocProject/Help/*.HxC
163 | DocProject/Help/*.hhc
164 | DocProject/Help/*.hhk
165 | DocProject/Help/*.hhp
166 | DocProject/Help/Html2
167 | DocProject/Help/html
168 |
169 | # Click-Once directory
170 | publish/
171 |
172 | # Publish Web Output
173 | *.[Pp]ublish.xml
174 | *.azurePubxml
175 | # Note: Comment the next line if you want to checkin your web deploy settings,
176 | # but database connection strings (with potential passwords) will be unencrypted
177 | *.pubxml
178 | *.publishproj
179 |
180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
181 | # checkin your Azure Web App publish settings, but sensitive information contained
182 | # in these scripts will be unencrypted
183 | PublishScripts/
184 |
185 | # NuGet Packages
186 | *.nupkg
187 | # The packages folder can be ignored because of Package Restore
188 | **/[Pp]ackages/*
189 | # except build/, which is used as an MSBuild target.
190 | !**/[Pp]ackages/build/
191 | # Uncomment if necessary however generally it will be regenerated when needed
192 | #!**/[Pp]ackages/repositories.config
193 | # NuGet v3's project.json files produces more ignorable files
194 | *.nuget.props
195 | *.nuget.targets
196 |
197 | # Microsoft Azure Build Output
198 | csx/
199 | *.build.csdef
200 |
201 | # Microsoft Azure Emulator
202 | ecf/
203 | rcf/
204 |
205 | # Windows Store app package directories and files
206 | AppPackages/
207 | BundleArtifacts/
208 | Package.StoreAssociation.xml
209 | _pkginfo.txt
210 | *.appx
211 |
212 | # Visual Studio cache files
213 | # files ending in .cache can be ignored
214 | *.[Cc]ache
215 | # but keep track of directories ending in .cache
216 | !?*.[Cc]ache/
217 |
218 | # Others
219 | ClientBin/
220 | ~$*
221 | *~
222 | *.dbmdl
223 | *.dbproj.schemaview
224 | *.jfm
225 | *.pfx
226 | *.publishsettings
227 | orleans.codegen.cs
228 |
229 | # Including strong name files can present a security risk
230 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
231 | #*.snk
232 |
233 | # Since there are multiple workflows, uncomment next line to ignore bower_components
234 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
235 | #bower_components/
236 |
237 | # RIA/Silverlight projects
238 | Generated_Code/
239 |
240 | # Backup & report files from converting an old project file
241 | # to a newer Visual Studio version. Backup files are not needed,
242 | # because we have git ;-)
243 | _UpgradeReport_Files/
244 | Backup*/
245 | UpgradeLog*.XML
246 | UpgradeLog*.htm
247 | ServiceFabricBackup/
248 | *.rptproj.bak
249 |
250 | # SQL Server files
251 | *.mdf
252 | *.ldf
253 | *.ndf
254 |
255 | # Business Intelligence projects
256 | *.rdl.data
257 | *.bim.layout
258 | *.bim_*.settings
259 | *.rptproj.rsuser
260 | *- Backup*.rdl
261 |
262 | # Microsoft Fakes
263 | FakesAssemblies/
264 |
265 | # GhostDoc plugin setting file
266 | *.GhostDoc.xml
267 |
268 | # Node.js Tools for Visual Studio
269 | .ntvs_analysis.dat
270 | node_modules/
271 |
272 | # Visual Studio 6 build log
273 | *.plg
274 |
275 | # Visual Studio 6 workspace options file
276 | *.opt
277 |
278 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
279 | *.vbw
280 |
281 | # Visual Studio LightSwitch build output
282 | **/*.HTMLClient/GeneratedArtifacts
283 | **/*.DesktopClient/GeneratedArtifacts
284 | **/*.DesktopClient/ModelManifest.xml
285 | **/*.Server/GeneratedArtifacts
286 | **/*.Server/ModelManifest.xml
287 | _Pvt_Extensions
288 |
289 | # Paket dependency manager
290 | .paket/paket.exe
291 | paket-files/
292 |
293 | # FAKE - F# Make
294 | .fake/
295 |
296 | # JetBrains Rider
297 | .idea/
298 | *.sln.iml
299 |
300 | # CodeRush personal settings
301 | .cr/personal
302 |
303 | # Python Tools for Visual Studio (PTVS)
304 | __pycache__/
305 | *.pyc
306 |
307 | # Cake - Uncomment if you are using it
308 | # tools/**
309 | # !tools/packages.config
310 |
311 | # Tabs Studio
312 | *.tss
313 |
314 | # Telerik's JustMock configuration file
315 | *.jmconfig
316 |
317 | # BizTalk build output
318 | *.btp.cs
319 | *.btm.cs
320 | *.odx.cs
321 | *.xsd.cs
322 |
323 | # OpenCover UI analysis results
324 | OpenCover/
325 |
326 | # Azure Stream Analytics local run output
327 | ASALocalRun/
328 |
329 | # MSBuild Binary and Structured Log
330 | *.binlog
331 |
332 | # NVidia Nsight GPU debugger configuration file
333 | *.nvuser
334 |
335 | # MFractors (Xamarin productivity tool) working folder
336 | .mfractor/
337 |
338 | # Local History for Visual Studio
339 | .localhistory/
340 |
341 | # BeatPulse healthcheck temp database
342 | healthchecksdb
343 |
344 | # BuildInfo
345 | WUView/BuildInfo.cs
346 |
347 | # Inno Setup backup
348 | *.~is
--------------------------------------------------------------------------------
/WUView/Dialogs/MDCustMsgBox.xaml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
19 |
20 |
21 |
22 |
31 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
68 |
69 |
70 |
71 |
72 |
73 |
80 |
89 |
90 |
91 |
92 |
93 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
108 |
109 |
111 |
112 |
117 |
118 |
119 |
120 |
121 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
139 |
144 |
149 |
154 |
155 |
156 |
157 |
158 |
159 |
--------------------------------------------------------------------------------
/WUView/ReadMe.txt:
--------------------------------------------------------------------------------
1 | ReadMe file for Windows Update Viewer
2 |
3 |
4 | Introduction
5 | ============
6 | Windows Update Viewer is an application that consolidates information about Windows Updates.
7 | Windows Update Viewer uses the Windows Update API and event logs to display details of installed
8 | updates. Event log entries are associated with individual updates by using the "KB" number. If an
9 | update does not use a KB number, or it isn't presented in a recognized format, no event log entries
10 | will be displayed. Windows Update Viewer can view the updates on the machine it is running from.
11 | At the present time, it cannot connect to a remote machine. It cannot download, apply or remove
12 | updates. The built-in Windows Update client does a respectable job of that.
13 |
14 |
15 | Getting Started
16 | ===============
17 | After installing Windows Update Viewer, launch it from the Start menu. You will see that the window
18 | has a grid on the top and a details pane on the bottom. Select a row in the grid and details for
19 | that update will be displayed in the details pane. The details pane can be closed and opened by
20 | selecting Toggle Details Pane on the View menu. The details pane can be resized by dragging the
21 | splitter up or down.
22 |
23 |
24 | Navigation
25 | ==========
26 | Use the navigation bar on the left side of the application to go to the settings or about pages, or
27 | to exit the application.
28 |
29 |
30 | Filtering the Grid
31 | ==================
32 | There is a text box at the top of the main window. Typing in this box will add a filter to any of
33 | the text columns in the grid. The Date column is not included. For example, if you only want to
34 | see the updates for the malicious software remover tool, then type "malicious" in the filter text
35 | box. The grid will update as you type. To see all of the updates, simply clear the filter text box.
36 | The filter will be reset each time Windows Update Viewer is started.
37 |
38 | As of version 0.5.10, starting the filter text with a "!" (exclamation mark or bang) will invert the
39 | filter. The filter will then act as an exclusionary filter. The bang itself along with any spaces
40 | between the bang and the first non-space character are discarded. If you wanted to quickly filter
41 | for updates that didn't have "succeeded" as a result, you could enter "!succeeded" in the filter
42 | box. The Escape key will clear any text from the filter box.
43 |
44 |
45 | The Exclude List
46 | ================
47 | To exclude updates without the need to type a filter every time, there is an exclude list. To access
48 | it select Edit Exclude List from the Settings page or by pressing Ctrl + L. A small dialog will open.
49 | This window is where you can specify strings to be excluded. When you first open this window you will
50 | see that "Defender" has already been added. Consequently, any update that has the word "Defender" in
51 | the update title will be excluded. Feel free to add any other strings to this list. For example, if
52 | you don't want to see the malicious software remover tool you could add "malicious" to the list. The
53 | words on this list are not case sensitive. Click OK to save any changes to the exclude list. The
54 | exclude list will be saved and reapplied the next time Windows Update Viewer is started.
55 |
56 | If you want to temporarily see all the installed updates, you can select Toggle Excluded Items from
57 | the View Menu or by pressing Ctrl + E instead of removing everything from the exclude list.
58 |
59 |
60 | Save to a File
61 | ==============
62 | You may save the details for all updates that haven't been excluded to a text file by selecting Save
63 | Details to Text File from the File menu. You can also save the current grid data to a CSV file.
64 |
65 |
66 | Settings Page
67 | =============
68 | Other options on the navigation bar include the Settings dialog. You can select between Light,
69 | Material Dark, Darker and System themes. You can select the accent color and you can choose from seven
70 | size options for the app. You can choose from three options for row spacing in the grid. You can also
71 | control the visibility of the Details pane and to show or hide updated that match items on the exclude
72 | list. You can also choose if the exclude process looks at the KB Number and Result fields as well as the
73 | Title. You can choose to have updates with the current date shown in bold. There are also options to
74 | have WUView stay on top of other windows and you can control the detail level of the log file. The bottom
75 | section of the settings page allows the language used in the user interface to be changed to one of the
76 | defined languages. Note that changing the language will cause WUView to restart immediately.
77 |
78 |
79 | About Page
80 | ==========
81 | Selecting About will display the About dialog which shows information about the app such as the version
82 | number and has a link to the GitHub repository. There is also a link to this read me file. You can also
83 | check for new releases of this application by clicking the link at the bottom of the About page. At the
84 | bottom of the About page there is a scrollable list of the people that contributed a language to help
85 | make Windows Update Viewer available to more users.
86 |
87 |
88 | Keyboard Shortcuts
89 | ==================
90 | These keyboard shortcuts are available:
91 |
92 | F1 = Go to the About screen
93 | F5 = Check for new updates and refresh the grid
94 | ESC = Removes any filter (while on the viewer page)
95 | Ctrl + D = Toggle the Details pane
96 | Ctrl + E = Toggle display of excluded items
97 | Ctrl + L = Open the Excludes Editor
98 | Ctrl + R = Reset column sort
99 | Ctrl + T = Change the date & time format
100 | Ctrl + U = Navigate to the Updates page
101 | Ctrl + Comma = Go to Settings
102 | Ctrl + Numpad Plus = Increase size
103 | Ctrl + Numpad Minus = Decrease size
104 | Ctrl + Shift + C = Change the Accent Color
105 | Ctrl + Shift + T = Change the Theme
106 |
107 |
108 | Uninstalling
109 | ============
110 | To uninstall use the regular Windows add/remove programs feature.
111 |
112 |
113 | Notices and License
114 | ===================
115 | Windows Update Viewer was written by Tim Kennedy.
116 |
117 | Windows Update Viewer uses the following packages and applications:
118 |
119 | * Material Design in XAML Toolkit https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit
120 |
121 | * Community Toolkit MVVM https://github.com/CommunityToolkit/dotnet
122 |
123 | * NLog https://nlog-project.org/
124 |
125 | * OctoKit https://github.com/octokit/octokit.net
126 |
127 | * NerdBank.GitVersioning https://github.com/dotnet/Nerdbank.GitVersioning
128 |
129 | * ResX Resource Manager was used for localization in earlier versions. https://github.com/dotnet/ResXResourceManager
130 |
131 | * GitKraken was used for everything Git related. https://www.gitkraken.com/
132 |
133 | * Inno Setup was used to create the installer. https://jrsoftware.org/isinfo.php
134 |
135 | * Visual Studio Community was used throughout the development of Windows Update Viewer. https://visualstudio.microsoft.com/vs/community/
136 |
137 | * XAML Styler is indispensable when working with XAML. https://github.com/Xavalon/XamlStyler
138 |
139 | * And of course, the essential PowerToys https://github.com/microsoft/PowerToys
140 |
141 | MIT License
142 | Copyright (c) 2023 Tim Kennedy
143 |
144 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
145 | associated documentation files (the "Software"), to deal in the Software without restriction, including
146 | without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
147 | sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject
148 | to the following conditions:
149 |
150 | The above copyright notice and this permission notice shall be included in all copies or substantial
151 | portions of the Software.
152 |
153 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
154 | LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
155 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
156 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
157 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
158 |
--------------------------------------------------------------------------------
/WUView/App.xaml.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Tim Kennedy. All Rights Reserved. Licensed under the MIT License.
2 |
3 | namespace WUView;
4 |
5 | ///
6 | /// Interaction logic for App.xaml
7 | ///
8 | public partial class App : Application
9 | {
10 | #region Properties
11 | ///
12 | /// Number of language strings in the test resource dictionary
13 | ///
14 | private static int TestLanguageStrings { get; set; }
15 |
16 | ///
17 | /// Uri of the test resource dictionary
18 | ///
19 | private static string? TestLanguageFile { get; set; }
20 |
21 | ///
22 | /// Number of language strings in the default resource dictionary
23 | ///
24 | public static int DefaultLanguageStrings { get; private set; }
25 | #endregion Properties
26 |
27 | #region On startup event
28 | ///
29 | /// Override the Startup Event.
30 | ///
31 | protected override void OnStartup(StartupEventArgs e)
32 | {
33 | base.OnStartup(e);
34 |
35 | // Unhandled exception handler
36 | AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
37 |
38 | // Only allows a single instance of the application to run.
39 | SingleInstance.Create(AppInfo.AppName);
40 |
41 | // Initialize settings here so that saved language can be accessed below.
42 | ConfigHelpers.InitializeSettings();
43 |
44 | // Log startup messages
45 | MainWindowHelpers.LogStartup();
46 |
47 | // Set the UI language
48 | SetLanguage();
49 |
50 | // Enable language testing if requested.
51 | CheckLanguageTesting();
52 | }
53 | #endregion On startup event
54 |
55 | #region Set the UI language
56 | ///
57 | /// Set the UI language.
58 | ///
59 | ///
60 | /// Strings.en-US.xaml is loaded in App.xaml as the fallback language.
61 | /// Consequently there is no need to explicitly load it in case of an error.
62 | ///
63 | private void SetLanguage()
64 | {
65 | // Get the number of strings in the default language file
66 | DefaultLanguageStrings = GetTotalDefaultLanguageCount();
67 |
68 | // Resource dictionary for language
69 | ResourceDictionary LanguageDictionary = [];
70 |
71 | // Log culture info at startup
72 | _log.Debug($"Startup culture: {LocalizationHelpers.GetCurrentCulture()} UI: {LocalizationHelpers.GetCurrentUICulture()}");
73 |
74 | // Get the current UI language
75 | string currentLanguage = Thread.CurrentThread.CurrentUICulture.Name;
76 |
77 | // Check the UseOSLanguage setting. If true try to use the language. Do not change current culture.
78 | if (LocalizationHelpers.CheckUseOsLanguage(currentLanguage))
79 | {
80 | if (currentLanguage == "en-US")
81 | {
82 | LocalizationHelpers.LanguageStrings = DefaultLanguageStrings;
83 | _log.Debug("Use OS Language option is true. Language is en-US. No need to load language file.");
84 | return;
85 | }
86 | try
87 | {
88 | LanguageDictionary.Source = new Uri($"Languages/Strings.{currentLanguage}.xaml", UriKind.RelativeOrAbsolute);
89 | Resources.MergedDictionaries.Add(LanguageDictionary);
90 | _log.Debug($"Use OS Language option is true. Language {currentLanguage} loaded.");
91 | }
92 | catch (Exception ex)
93 | {
94 | LanguageDictionary.Source = new Uri("Languages/Strings.en-US.xaml", UriKind.RelativeOrAbsolute);
95 | _log.Warn(ex, $"Language {currentLanguage} could not be located. Defaulting to en-US");
96 | }
97 | LocalizationHelpers.ApplyLanguageSettings(LanguageDictionary);
98 | return;
99 | }
100 |
101 | // If a language is defined in settings, and it exists in the list of defined languages, set the current culture and language to it.
102 | if (!string.IsNullOrEmpty(UserSettings.Setting!.UILanguage) &&
103 | UILanguage.DefinedLanguages.Exists(x => x.LanguageCode == UserSettings.Setting.UILanguage))
104 | {
105 | try
106 | {
107 | LanguageDictionary.Source = new Uri($"Languages/Strings.{UserSettings.Setting.UILanguage}.xaml", UriKind.RelativeOrAbsolute);
108 | Thread.CurrentThread.CurrentCulture = new CultureInfo(UserSettings.Setting.UILanguage);
109 | Thread.CurrentThread.CurrentUICulture = new CultureInfo(UserSettings.Setting.UILanguage);
110 | Resources.MergedDictionaries.Add(LanguageDictionary);
111 | }
112 | catch (Exception ex)
113 | {
114 | LanguageDictionary.Source = new Uri("Languages/Strings.en-US.xaml", UriKind.RelativeOrAbsolute);
115 | _log.Warn(ex, $"Error using language \"{UserSettings.Setting.UILanguage}\". Defaulting to en-US");
116 | }
117 | LocalizationHelpers.ApplyLanguageSettings(LanguageDictionary);
118 | return;
119 | }
120 |
121 | // If language is not found in settings, or the language is not defined in UILanguage.DefinedLanguages, use en-US.
122 | // Strings.en-US.xaml is loaded in App.xaml therefore there is no need to explicitly load it here.
123 | LanguageDictionary.Source = new Uri("Languages/Strings.en-US.xaml", UriKind.RelativeOrAbsolute);
124 | UserSettings.Setting.UILanguage = "en-US";
125 | ConfigHelpers.SaveSettings();
126 | _log.Warn("Language defaulting to en-US");
127 | LocalizationHelpers.ApplyLanguageSettings(LanguageDictionary);
128 | }
129 | #endregion Set the UI language
130 |
131 | #region Language testing
132 | private void CheckLanguageTesting()
133 | {
134 | if (UserSettings.Setting!.LanguageTesting)
135 | {
136 | _log.Info("Language testing enabled");
137 | ResourceDictionary testDict = [];
138 | string testLanguageFile = Path.Combine(AppInfo.AppDirectory, "Strings.test.xaml");
139 | if (File.Exists(testLanguageFile))
140 | {
141 | try
142 | {
143 | testDict.Source = new Uri(testLanguageFile, UriKind.RelativeOrAbsolute);
144 | if (testDict.Source != null)
145 | {
146 | Resources.MergedDictionaries.Add(testDict);
147 | TestLanguageStrings = testDict.Count;
148 | TestLanguageFile = testDict.Source.OriginalString;
149 | _log.Debug($"{TestLanguageStrings} strings loaded from {TestLanguageFile}");
150 | }
151 | }
152 | catch (Exception ex)
153 | {
154 | _log.Error(ex, $"Error loading test language file {TestLanguageFile}");
155 | string msg = string.Format(CultureInfo.CurrentCulture,
156 | $"{GetStringResource("MsgText_Error_TestLanguage")}\n\n{ex.Message}\n\n{ex.InnerException}");
157 | _ = MessageBox.Show(msg,
158 | GetStringResource("MsgText_ErrorCaption"),
159 | MessageBoxButton.OK,
160 | MessageBoxImage.Error);
161 | }
162 | }
163 | else
164 | {
165 | _log.Error($"Error loading test language file {TestLanguageFile}. File not found.");
166 | }
167 | }
168 | }
169 | #endregion Language testing
170 |
171 | #region Unhandled Exception Handler
172 | ///
173 | /// Handles any exceptions that weren't caught by a try-catch statement.
174 | ///
175 | ///
176 | /// This uses default message box.
177 | ///
178 | private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs args)
179 | {
180 | _log.Error("Unhandled Exception");
181 | Exception e = (Exception)args.ExceptionObject;
182 | _log.Error(e.Message);
183 | if (e.InnerException != null)
184 | {
185 | _log.Error(e.InnerException.ToString());
186 | }
187 | _log.Error(e.StackTrace);
188 |
189 | _ = MessageBox.Show($"An error has occurred.\n{e.Message}\n\nSee the log file. ",
190 | GetStringResource("MsgText_ErrorCaption"),
191 | MessageBoxButton.OK,
192 | MessageBoxImage.Error);
193 | }
194 | #endregion Unhandled Exception Handler
195 | }
196 |
--------------------------------------------------------------------------------
/WUView/Helpers/FileHelpers.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Tim Kennedy. All Rights Reserved. Licensed under the MIT License.
2 |
3 | namespace WUView.Helpers;
4 |
5 | ///
6 | /// Methods for reading and writing files
7 | ///
8 | public static partial class FileHelpers
9 | {
10 | #region JSON options
11 | private static JsonSerializerOptions JsonOptions { get; } = new()
12 | {
13 | WriteIndented = true
14 | };
15 | #endregion JSON options
16 |
17 | #region Read the Exclude file
18 | ///
19 | /// Read the JSON file containing the exclude items
20 | ///
21 | public static void GetExcludes()
22 | {
23 | Stopwatch rxStopWatch = Stopwatch.StartNew();
24 | string xFile = GetExcludesFile();
25 | string json = File.ReadAllText(xFile);
26 | ExcludedItems.ExcludedStrings = JsonSerializer.Deserialize>(json)!;
27 | rxStopWatch.Stop();
28 | int xCount = ExcludedItems.ExcludedStrings!.Count;
29 | string xRecs = xCount == 1 ? "record" : "records";
30 | _log.Debug($"Read {ExcludedItems.ExcludedStrings.Count} exclude {xRecs} from {xFile} in {rxStopWatch.Elapsed.TotalMilliseconds:N2} milliseconds");
31 | if (xCount > 0)
32 | {
33 | foreach (ExcludedItems item in ExcludedItems.ExcludedStrings)
34 | {
35 | _log.Info($"{GetStringResource("MsgText_ExcludingContaining")} \"{item.ExcludedString}\"");
36 | }
37 | }
38 | }
39 | #endregion Read the Exclude file
40 |
41 | #region Save the Exclude file
42 | ///
43 | /// Save the JSON file containing the exclude items
44 | ///
45 | public static async Task SaveExcludeFile()
46 | {
47 | string json = JsonSerializer.Serialize(ExcludedItems.ExcludedStrings, JsonOptions);
48 | await File.WriteAllTextAsync(GetExcludesFile(), json);
49 | _log.Info($"Saving {GetExcludesFile()}");
50 |
51 | foreach (ExcludedItems item in ExcludedItems.ExcludedStrings)
52 | {
53 | _log.Info($"Excluding updates containing: \"{item.ExcludedString}\"");
54 | }
55 | }
56 | #endregion Save the Exclude file
57 |
58 | #region Get the exclude file name
59 | ///
60 | /// Determine the full path for the exclude file. Create it if it doesn't exist.
61 | ///
62 | /// Full path to exclude file
63 | public static string GetExcludesFile()
64 | {
65 | string filePath = Path.Combine(AppInfo.AppDirectory, "WUViewExcludes.json");
66 | if (!File.Exists(filePath))
67 | {
68 | File.Create(filePath).Dispose();
69 | File.WriteAllText(filePath, "[{ \"ExcludedString\": \"Defender\"}]");
70 | _log.Debug($"New Exclude file created: {filePath}");
71 | }
72 | return filePath;
73 | }
74 | #endregion Get the exclude file name
75 |
76 | #region Save grid to CSV file
77 | ///
78 | /// Save the contents of the DataGrid to a CSV file
79 | ///
80 | public static async Task SaveToCSV()
81 | {
82 | string filename = "WUView_" + DateTime.Now.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + ".csv";
83 | SaveFileDialog dialog = new()
84 | {
85 | Title = ResourceHelpers.GetStringResource("MenuItem_SaveCSV"),
86 | Filter = "CSV File|*.csv",
87 | InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
88 | FileName = filename
89 | };
90 | bool? result = dialog.ShowDialog();
91 | if (result == true)
92 | {
93 | MainPage.Instance!.Copy2Clipboard();
94 | string gridData = (string)Clipboard.GetData(DataFormats.CommaSeparatedValue);
95 | await File.WriteAllTextAsync(dialog.FileName, gridData, Encoding.UTF8);
96 | _log.Debug($"Details written to {dialog.FileName}");
97 | }
98 | }
99 | #endregion Save grid to CSV file
100 |
101 | #region Save details to a text file
102 | ///
103 | /// Saves details to a text file.
104 | ///
105 | public static async Task SaveToFile()
106 | {
107 | string filename = "WUView_" + DateTime.Now.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + ".txt";
108 | SaveFileDialog dialog = new()
109 | {
110 | Title = GetStringResource("MenuItem_SaveTXT"),
111 | Filter = "Text File|*.txt",
112 | InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
113 | FileName = filename
114 | };
115 | bool? result = dialog.ShowDialog();
116 | if (result == true)
117 | {
118 | StringBuilder sb = new();
119 | _ = sb.Append(GetStringResource("Details_HeadingUpdate"))
120 | .Append(' ')
121 | .Append(Environment.MachineName)
122 | .Append(" - ")
123 | .AppendFormat(CultureInfo.InvariantCulture, "{0:G}", DateTime.Now)
124 | .AppendLine();
125 | string underscore = new('-', sb.Length - 2);
126 | _ = sb.Append(underscore).AppendLine("\r\n");
127 |
128 | List listInUse = [.. MainViewModel.UpdatesFullList];
129 | if (UserSettings.Setting!.HideExcluded)
130 | {
131 | listInUse = [.. MainViewModel.UpdatesWithoutExcludedItems];
132 | }
133 |
134 | for (int i = 0; i < listInUse.Count; i++)
135 | {
136 | _ = sb.Append(GetStringResource("Details_Title"))
137 | .Append(' ')
138 | .AppendLine(listInUse[i].Title)
139 | .Append(GetStringResource("Details_Date"))
140 | .Append(' ')
141 | .AppendLine(listInUse[i].Date.ToString(CultureInfo.InvariantCulture))
142 | .Append(GetStringResource("Details_KBNum"))
143 | .Append(' ')
144 | .AppendLine(listInUse[i].KBNum)
145 | .Append(GetStringResource("Details_Operation"))
146 | .Append(' ')
147 | .AppendLine(listInUse[i].Operation!.Replace("uo", ""))
148 | .Append(GetStringResource("Details_ResultCode"))
149 | .Append(' ')
150 | .AppendLine(listInUse[i].ResultCode!.Replace("orc", ""))
151 | .Append(GetStringResource("Details_HResult"))
152 | .Append(' ')
153 | .AppendFormat(CultureInfo.InvariantCulture, $"0x{int.Parse(listInUse[i].HResult!, CultureInfo.InvariantCulture):X8}")
154 | .AppendLine()
155 | .Append(GetStringResource("Details_UpdateID"))
156 | .Append(' ')
157 | .AppendLine(listInUse[i].UpdateID)
158 | .Append(GetStringResource("Details_SupportURL"))
159 | .Append(' ')
160 | .AppendLine(listInUse[i].SupportURL)
161 | .Append(GetStringResource("Details_Description"))
162 | .Append(' ')
163 | .AppendLine(listInUse[i].Description);
164 |
165 | foreach (string line in RemoveCarriageReturnLineFeed().Split(MainViewModel.FindEventLogs(listInUse[i].KBNum!)))
166 | {
167 | if (!string.IsNullOrWhiteSpace(line))
168 | {
169 | _ = sb.Append(GetStringResource("Details_HeadingEventLog")).Append(' ').AppendLine(line);
170 | }
171 | }
172 | _ = sb.AppendLine("\r\n");
173 | }
174 | await File.WriteAllTextAsync(dialog.FileName, sb.ToString());
175 | _ = sb.Clear();
176 | _log.Debug($"Details written to {dialog.FileName}");
177 | }
178 | }
179 | [GeneratedRegex("\r\n|\r|\n")]
180 | private static partial Regex RemoveCarriageReturnLineFeed();
181 | #endregion Save details to a text file
182 |
183 | #region Save updates as JSON
184 | ///
185 | /// Save entire history as JSON file
186 | ///
187 | public static async Task SaveAsJson()
188 | {
189 | string filename = "WUView_Export_" + DateTime.Now.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + ".json";
190 | SaveFileDialog dialog = new()
191 | {
192 | Title = GetStringResource("MenuItem_SaveJSON"),
193 | Filter = "JSON File|*.json",
194 | InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
195 | FileName = filename
196 | };
197 | bool? result = dialog.ShowDialog();
198 | if (result == true)
199 | {
200 | string json = JsonSerializer.Serialize(MainViewModel.UpdatesFullList, JsonOptions);
201 | await File.WriteAllTextAsync(dialog.FileName, json);
202 | _log.Debug($"History exported to {dialog.FileName}");
203 | }
204 | }
205 | #endregion Save updates as JSON
206 | }
207 |
--------------------------------------------------------------------------------
/WUView/Inno_Setup/WUViewEx.iss:
--------------------------------------------------------------------------------
1 | ; ---------------------------------------------------------------------
2 | ; Inno Setup Script for Windows Update Viewer (WUView)
3 | ;----------------------------------------------------------------------
4 | ; The following #include file is created by the PubSetupEx.ps1 script.
5 | ;
6 | ; It contains #define statements for:
7 | ;
8 | ; InstallType: String denoting installer type.
9 | ; Inserted into the installer file name.
10 | ;
11 | ; PublishFolder: The output folder from MS Build.
12 | ; Varies depending on the type of build.
13 | ;----------------------------------------------------------------------
14 | #include "D:\Temp\PubSetup.Temp.iss"
15 |
16 | #define BaseDir "V:\Source\Repos\WUView\WUView"
17 | #define MySourceDir BaseDir + PublishFolder
18 | #define MySetupIcon BaseDir + "\Images\UV.ico"
19 | #define MyOutputDir "D:\InnoSetup\Output"
20 | #define MyLargeImage "D:\InnoSetup\Images\WizardImageWUV.bmp"
21 | #define MySmallImage "D:\InnoSetup\Images\WizardSmallImage.bmp"
22 |
23 | #define MyAppID "{3A152885-8378-4FDE-AFCC-85D096B16A1D}"
24 | #define MyAppName "Windows Update Viewer"
25 | #define MyAppNameNoSpaces StringChange(MyAppName, " ", "")
26 | #define MyAppExeName "WUView.exe"
27 | #define MyAppVersion GetVersionNumbersString(MySourceDir + "\" + MyAppExeName)
28 | #define MyInstallerFilename MyAppNameNoSpaces + "_" + MyAppVersion + "_" + InstallType + "_Setup"
29 | #define MyCompanyName "T_K"
30 | #define MyPublisherName "Tim Kennedy"
31 | #define StartCopyrightYear "2019"
32 | #define CurrentYear GetDateTimeString('yyyy', '/', ':')
33 | #define MyCopyright "(c) " + StartCopyrightYear + "-" + CurrentYear + " Tim Kennedy"
34 | #define MyLicFile "D:\Visual Studio\Resources\License.rtf"
35 | #define MyDateTimeString GetDateTimeString('yyyy/mm/dd hh:nn:ss', '/', ':')
36 | #define MyAppSupportURL "https://github.com/Timthreetwelve/WUView"
37 | #define RunRegKey "Software\Microsoft\Windows\CurrentVersion\Run"
38 |
39 | ; -----------------------------------------------------
40 | ; Include the localization file. Thanks bovirus!
41 | ; -----------------------------------------------------
42 | #include "WUViewLocalization.iss"
43 |
44 |
45 | [Setup]
46 | ; NOTE: The value of AppId uniquely identifies this application.
47 | ; Do not use the same AppId value in installers for other applications.
48 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
49 | ;---------------------------------------------
50 | AppId={{#MyAppID}
51 |
52 | ;---------------------------------------------
53 | ; Uncomment the following line to run in non administrative install mode (install for current user only.)
54 | ; Installs in %localappdata%\Programs\ instead of \Program Files(x86)
55 | ;---------------------------------------------
56 | PrivilegesRequired=lowest
57 | ;---------------------------------------------
58 |
59 | AppName={#MyAppName}
60 | AppVersion={#MyAppVersion}
61 | AppVerName={#MyAppName} {#MyAppVersion}
62 |
63 | AppCopyright={#MyCopyright}
64 | AppPublisherURL={#MyAppSupportURL}
65 | AppSupportURL={#MyAppSupportURL}
66 | AppUpdatesURL={#MyAppSupportURL}
67 |
68 | VersionInfoDescription={#MyAppName} installer
69 | VersionInfoProductName={#MyAppName}
70 | VersionInfoVersion={#MyAppVersion}
71 |
72 | UninstallDisplayName={#MyAppName}
73 | UninstallDisplayIcon={app}\{#MyAppExeName}
74 | AppPublisher={#MyPublisherName}
75 |
76 | ShowLanguageDialog=yes
77 | UsePreviousLanguage=no
78 | WizardStyle=modern
79 | WizardSizePercent=100,100
80 | WizardImageFile={#MyLargeImage}
81 | WizardSmallImageFile={#MySmallImage}
82 |
83 | AllowNoIcons=yes
84 | Compression=lzma
85 | DefaultDirName={autopf}\{#MyCompanyName}\{#MyAppName}
86 | DefaultGroupName={#MyAppName}
87 | DisableDirPage=yes
88 | DisableProgramGroupPage=yes
89 | DisableReadyMemo=no
90 | DisableStartupPrompt=yes
91 | DisableWelcomePage=no
92 | OutputBaseFilename={#MyInstallerFilename}
93 | OutputDir={#MyOutputDir}
94 | ;OutputManifestFile={#MyAppName}_{#MyAppVersion}_{#InstallType}_FileList.txt
95 | SetupIconFile={#MySetupIcon}
96 | SetupLogging=yes
97 | SolidCompression=no
98 | SourceDir={#MySourceDir}
99 |
100 | [Files]
101 | Source: "{#MySourceDir}\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
102 | Source: "{#MySourceDir}\*.dll"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
103 | Source: "{#MySourceDir}\*.json"; Excludes: "usersettings.json"; DestDir: "{app}"; Flags: ignoreversion
104 | Source: "{#MySourceDir}\ReadMe.txt"; DestDir: "{app}"; Flags: ignoreversion
105 | Source: "{#MySourceDir}\License.txt"; DestDir: "{app}"; Flags: ignoreversion
106 | Source: "{#MySourceDir}\Strings.test.xaml"; DestDir: "{app}"; Flags: ignoreversion
107 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files
108 |
109 | [InstallDelete]
110 | ; Delete these files & folders from previous installs
111 | Type: filesandordirs; Name: "{group}"
112 | Type: files; Name: "{app}\Nlog.config"
113 | Type: files; Name: "{app}\Newtonsoft.Json.dll"
114 |
115 | [Icons]
116 | Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
117 |
118 | [Registry]
119 | Root: HKCU; Subkey: "Software\{#MyCompanyName}"; Flags: uninsdeletekeyifempty
120 | Root: HKCU; Subkey: "Software\{#MyCompanyName}\{#MyAppName}"; Flags: uninsdeletekey
121 | Root: HKCU; Subkey: "Software\{#MyCompanyName}\{#MyAppName}"; ValueType: string; ValueName: "Copyright"; ValueData: "{#MyCopyright}"; Flags: uninsdeletekey
122 | Root: HKCU; Subkey: "Software\{#MyCompanyName}\{#MyAppName}"; ValueType: string; ValueName: "Install Date"; ValueData: "{#MyDateTimeString}"; Flags: uninsdeletekey
123 | Root: HKCU; Subkey: "Software\{#MyCompanyName}\{#MyAppName}"; ValueType: string; ValueName: "Version"; ValueData: "{#MyAppVersion}"; Flags: uninsdeletekey
124 | Root: HKCU; Subkey: "Software\{#MyCompanyName}\{#MyAppName}"; ValueType: string; ValueName: "Install Folder"; ValueData: "{autopf}\{#MyCompanyName}\{#MyAppName}"; Flags: uninsdeletekey
125 | Root: HKCU; Subkey: "Software\{#MyCompanyName}\{#MyAppName}"; ValueType: string; ValueName: "Installer Language"; ValueData:"{language}" ;Flags: uninsdeletekey
126 | ; Delete this key from previous installs
127 | Root: HKCU; Subkey: "Software\{#MyCompanyName}\{#MyAppName}"; ValueType: none; ValueName: "Edition"; Flags: uninsdeletekey deletevalue
128 |
129 | [Run]
130 | Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent shellexec
131 | Filename: "{app}\ReadMe.txt"; Description: "{cm:ViewReadme}"; Flags: nowait postinstall skipifsilent unchecked shellexec
132 |
133 | [UninstallRun]
134 | Filename: "{sys}\taskkill.exe"; Parameters: "/im {#MyAppExeName} /t /f"; RunOnceId: "DelService"; Flags: runhidden skipifdoesntexist
135 |
136 | [UninstallDelete]
137 | Type: files; Name: "{app}\*.txt"
138 |
139 | ; -----------------------------------------------------------------------------
140 | ; Code section follows
141 | ; -----------------------------------------------------------------------------
142 | [Code]
143 | // Change text on welcome page based on installation type
144 | procedure InitializeWizard;
145 | var
146 | Text: String;
147 | begin
148 | case ExpandConstant('{#InstallType}') of
149 | 'x64x86': Text := FmtMessage( CustomMessage('NotSelfContained'), [ExpandConstant('{#MyAppName}'), ExpandConstant('{#MyAppVersion}')]);
150 | 'SC_x86': Text := FmtMessage( CustomMessage('SelfContainedx86'), [ExpandConstant('{#MyAppName}'), ExpandConstant('{#MyAppVersion}')]);
151 | 'SC_x64': Text := FmtMessage( CustomMessage('SelfContainedx64'), [ExpandConstant('{#MyAppName}'), ExpandConstant('{#MyAppVersion}')]);
152 | else
153 | Text := WizardForm.WelcomeLabel2.Caption;
154 | end;
155 | WizardForm.WelcomeLabel2.Caption := Text;
156 | end;
157 |
158 | // function used to check if app is currently running
159 | function IsAppRunning(const FileName : string): Boolean;
160 | var
161 | FSWbemLocator: Variant;
162 | FWMIService : Variant;
163 | FWbemObjectSet: Variant;
164 | begin
165 | Result := false;
166 | FSWbemLocator := CreateOleObject('WBEMScripting.SWBEMLocator');
167 | FWMIService := FSWbemLocator.ConnectServer('', 'root\CIMV2', '', '');
168 | FWbemObjectSet :=
169 | FWMIService.ExecQuery(
170 | Format('SELECT Name FROM Win32_Process Where Name="%s"', [FileName]));
171 | Result := (FWbemObjectSet.Count > 0);
172 | FWbemObjectSet := Unassigned;
173 | FWMIService := Unassigned;
174 | FSWbemLocator := Unassigned;
175 | end;
176 |
177 | // Checks if app is running, if so, displays msgbox asking to close running app
178 | function InitializeSetup(): Boolean;
179 | var
180 | Answer: Integer;
181 | ThisApp: String;
182 | begin
183 | Result := true;
184 | ThisApp := ExpandConstant('{#MyAppExeName}');
185 | while IsAppRunning(ThisApp) do
186 | begin
187 | Answer := MsgBox(ThisApp + ' ' + CustomMessage('AppIsRunning'), mbError, MB_OKCANCEL);
188 | If Answer = IDCANCEL then
189 | begin
190 | Result := false;
191 | Exit;
192 | end;
193 | end;
194 | end;
195 |
196 | // Copies setup log to app folder
197 | procedure CurStepChanged(CurStep: TSetupStep);
198 | var
199 | logfilepathname, newfilepathname: string;
200 | begin
201 | if CurStep = ssDone then
202 | begin
203 | logfilepathname := ExpandConstant('{log}');
204 | newfilepathname := ExpandConstant('{app}\') + 'Setup_Log.txt';
205 | Log('Setup log file copied to: ' + newfilepathname);
206 | CopyFile(logfilepathname, newfilepathname, False);
207 | end;
208 | end;
209 |
210 | // Uninstall
211 | procedure CurUninstallStepChanged (CurUninstallStep: TUninstallStep);
212 | var
213 | mres : integer;
214 | begin
215 | case CurUninstallStep of
216 | usPostUninstall:
217 | begin
218 | mres := MsgBox(CustomMessage('DeleteConfigFiles'), mbConfirmation, MB_YESNO or MB_DEFBUTTON2)
219 | if mres = IDYES then
220 | begin
221 | DelTree(ExpandConstant('{app}\*.json'), False, True, False);
222 | DelTree(ExpandConstant('{app}'), True, True, True);
223 | end;
224 | end;
225 | end;
226 | end;
227 |
--------------------------------------------------------------------------------