// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using CommunityToolkit.WinUI.UI.Controls;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
using Windows.Foundation.Metadata;
using Windows.Storage;
namespace RegistryPreviewUILib
{
public sealed partial class RegistryPreviewMainPage : Page
{
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
// Indicator if we loaded/reloaded/saved a file and need to skip TextChanged event one time.
// (Solves the problem that enabling the event handler fires it one time.)
private static bool editorContentChangedScripted;
///
/// Event that is will prevent the app from closing if the "save file" flag is active
///
public void MainWindow_Closed(object sender, WindowEventArgs args)
{
// Only block closing if the REG file has been edited but not yet saved
if (saveButton.IsEnabled)
{
// if true, the app will not close
args.Handled = true;
// ask the user if they want to save, discard or cancel the close; strings must be loaded here and passed to avoid timing issues
HandleDirtyClosing(
resourceLoader.GetString("YesNoCancelDialogTitle"),
resourceLoader.GetString("YesNoCancelDialogContent"),
resourceLoader.GetString("YesNoCancelDialogPrimaryButtonText"),
resourceLoader.GetString("YesNoCancelDialogSecondaryButtonText"),
resourceLoader.GetString("YesNoCancelDialogCloseButtonText"));
}
}
///
/// Event that gets fired after the visual tree has been fully loaded; the app opens the reg file from here so it can show a message box successfully
///
private async void GridPreview_Loaded(object sender, RoutedEventArgs e)
{
// static flag to track whether the Visual Tree is ready - if the main Grid has been loaded, the tree is ready.
visualTreeReady = true;
// Check to see if the REG file was opened and parsed successfully
if (await OpenRegistryFile(_appFileName) == false)
{
if (File.Exists(_appFileName))
{
// Allow Refresh and Edit to be enabled because a broken Reg file might be fixable
UpdateToolBarAndUI(false, true, true);
_updateWindowTitleFunction(resourceLoader.GetString("InvalidRegistryFileTitle"));
MonacoEditor.TextChanged += MonacoEditor_TextChanged;
return;
}
else
{
UpdateToolBarAndUI(false, false, false);
_updateWindowTitleFunction(string.Empty);
}
}
else
{
MonacoEditor.TextChanged += MonacoEditor_TextChanged;
}
MonacoEditor.Focus(FocusState.Programmatic);
}
///
/// New button action: Ask to save last changes and reset editor content to reg header only
///
private async void NewButton_Click(object sender, RoutedEventArgs e)
{
// Check to see if the current file has been saved
if (saveButton.IsEnabled)
{
ContentDialog contentDialog = new ContentDialog()
{
Title = resourceLoader.GetString("YesNoCancelDialogTitle"),
Content = resourceLoader.GetString("YesNoCancelDialogContent"),
PrimaryButtonText = resourceLoader.GetString("YesNoCancelDialogPrimaryButtonText"),
SecondaryButtonText = resourceLoader.GetString("YesNoCancelDialogSecondaryButtonText"),
CloseButtonText = resourceLoader.GetString("YesNoCancelDialogCloseButtonText"),
DefaultButton = ContentDialogButton.Primary,
};
// Use this code to associate the dialog to the appropriate AppWindow by setting
// the dialog's XamlRoot to the same XamlRoot as an element that is already present in the AppWindow.
if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8))
{
contentDialog.XamlRoot = this.Content.XamlRoot;
}
ContentDialogResult contentDialogResult = await contentDialog.ShowAsync();
switch (contentDialogResult)
{
case ContentDialogResult.Primary:
// Save, then continue the new action
if (!AskFileName(string.Empty) ||
!SaveFile())
{
return;
}
break;
case ContentDialogResult.Secondary:
// Don't save and continue the new action!
break;
default:
// Don't open the new action!
return;
}
}
// mute the TextChanged handler to make for clean UI
MonacoEditor.TextChanged -= MonacoEditor_TextChanged;
// reset editor, file info and ui.
_appFileName = string.Empty;
ResetEditorAndFile();
// disable buttons that do not make sense
UpdateUnsavedFileState(false);
refreshButton.IsEnabled = false;
// restore the TextChanged handler
ButtonAction_RestoreTextChangedEvent();
}
///
/// Uses a picker to select a new file to open
///
private async void OpenButton_Click(object sender, RoutedEventArgs e)
{
// Check to see if the current file has been saved
if (saveButton.IsEnabled)
{
ContentDialog contentDialog = new ContentDialog()
{
Title = resourceLoader.GetString("YesNoCancelDialogTitle"),
Content = resourceLoader.GetString("YesNoCancelDialogContent"),
PrimaryButtonText = resourceLoader.GetString("YesNoCancelDialogPrimaryButtonText"),
SecondaryButtonText = resourceLoader.GetString("YesNoCancelDialogSecondaryButtonText"),
CloseButtonText = resourceLoader.GetString("YesNoCancelDialogCloseButtonText"),
DefaultButton = ContentDialogButton.Primary,
};
// Use this code to associate the dialog to the appropriate AppWindow by setting
// the dialog's XamlRoot to the same XamlRoot as an element that is already present in the AppWindow.
if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8))
{
contentDialog.XamlRoot = this.Content.XamlRoot;
}
ContentDialogResult contentDialogResult = await contentDialog.ShowAsync();
switch (contentDialogResult)
{
case ContentDialogResult.Primary:
// Save, then continue the file open
if (!AskFileName(string.Empty) ||
!SaveFile())
{
return;
}
break;
case ContentDialogResult.Secondary:
// Don't save and continue the file open!
break;
default:
// Don't open the new file!
return;
}
}
// Pull in a new REG file - we have to use the direct Win32 method because FileOpenPicker crashes when it's
// called while running as admin
IntPtr windowHandle = WinRT.Interop.WindowNative.GetWindowHandle(_mainWindow);
string filename = OpenFilePicker.ShowDialog(
windowHandle,
resourceLoader.GetString("FilterRegistryName") + '\0' + "*.reg" + '\0' + resourceLoader.GetString("FilterAllFiles") + '\0' + "*.*" + '\0' + '\0',
resourceLoader.GetString("OpenDialogTitle"));
if (filename == string.Empty || File.Exists(filename) == false)
{
return;
}
StorageFile storageFile = await StorageFile.GetFileFromPathAsync(filename);
if (storageFile != null)
{
// mute the TextChanged handler to make for clean UI
MonacoEditor.TextChanged -= MonacoEditor_TextChanged;
// update file name
_appFileName = storageFile.Path;
UpdateToolBarAndUI(await OpenRegistryFile(_appFileName));
// disable the Save button as it's a new file
UpdateUnsavedFileState(false);
// Restore the event handler as we're loaded
ButtonAction_RestoreTextChangedEvent();
}
}
///
/// Saves the currently opened file in place
///
private void SaveButton_Click(object sender, RoutedEventArgs e)
{
if (!AskFileName(string.Empty))
{
return;
}
// save and update window title
// error handling and ui update happens in SaveFile() method
_ = SaveFile();
}
///
/// Uses a picker to save out a copy of the current reg file
///
private async void SaveAsButton_Click(object sender, RoutedEventArgs e)
{
// mute the TextChanged handler to make for clean UI
MonacoEditor.TextChanged -= MonacoEditor_TextChanged;
if (!AskFileName(_appFileName) || !SaveFile())
{
return;
}
UpdateToolBarAndUI(await OpenRegistryFile(_appFileName));
// restore the TextChanged handler
ButtonAction_RestoreTextChangedEvent();
}
///
/// Reloads the current REG file from storage
///
private async void RefreshButton_Click(object sender, RoutedEventArgs e)
{
// Check to see if the current file has been saved
if (saveButton.IsEnabled)
{
ContentDialog contentDialog = new ContentDialog()
{
Title = resourceLoader.GetString("YesNoCancelDialogTitle"),
Content = resourceLoader.GetString("ReloadDialogContent"),
PrimaryButtonText = resourceLoader.GetString("ReloadDialogPrimaryButtonText"),
CloseButtonText = resourceLoader.GetString("ReloadDialogCloseButtonText"),
DefaultButton = ContentDialogButton.Primary,
};
// Use this code to associate the dialog to the appropriate AppWindow by setting
// the dialog's XamlRoot to the same XamlRoot as an element that is already present in the AppWindow.
if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8))
{
contentDialog.XamlRoot = this.Content.XamlRoot;
}
ContentDialogResult contentDialogResult = await contentDialog.ShowAsync();
switch (contentDialogResult)
{
case ContentDialogResult.Primary:
// Don't save and continue the reload action!
break;
default:
// Don't continue the reload action!
return;
}
}
// mute the TextChanged handler to make for clean UI
MonacoEditor.TextChanged -= MonacoEditor_TextChanged;
// reload the current Registry file and update the toolbar accordingly.
UpdateToolBarAndUI(await OpenRegistryFile(_appFileName), true, true);
// disable the Save button as it's a new file
UpdateUnsavedFileState(false);
// restore the TextChanged handler
ButtonAction_RestoreTextChangedEvent();
}
///
/// Opens the Registry Editor; UAC is handled by the request to open
///
private void RegistryButton_Click(object sender, RoutedEventArgs e)
{
// pass in an empty string as we have no file to open
OpenRegistryEditor(string.Empty);
}
///
/// Opens the Registry Editor and tries to set "last used"; UAC is handled by the request to open
///
private void RegistryJumpToKeyButton_Click(object sender, RoutedEventArgs e)
{
// Get the selected Key, if there is one
TreeViewNode currentNode = treeView.SelectedNode;
if (currentNode != null)
{
// since there is a valid node, get the FullPath of the key that was selected
string key = ((RegistryKey)currentNode.Content).FullPath;
// it's impossible to directly open a key via command-line option, so we must override the last remember key
Microsoft.Win32.Registry.SetValue(@"HKEY_Current_User\Software\Microsoft\Windows\CurrentVersion\Applets\Regedit", "LastKey", key);
}
// pass in an empty string as we have no file to open
OpenRegistryEditor(string.Empty);
}
///
/// Merges the currently saved file into the Registry Editor; UAC is handled by the request to open
///
private async void WriteButton_Click(object sender, RoutedEventArgs e)
{
// Check to see if the current file has been saved
if (saveButton.IsEnabled)
{
ContentDialog contentDialog = new ContentDialog()
{
Title = resourceLoader.GetString("YesNoCancelDialogTitle"),
Content = resourceLoader.GetString("YesNoCancelDialogContent"),
PrimaryButtonText = resourceLoader.GetString("YesNoCancelDialogPrimaryButtonText"),
SecondaryButtonText = resourceLoader.GetString("YesNoCancelDialogSecondaryButtonText"),
CloseButtonText = resourceLoader.GetString("YesNoCancelDialogCloseButtonText"),
DefaultButton = ContentDialogButton.Primary,
};
// Use this code to associate the dialog to the appropriate AppWindow by setting
// the dialog's XamlRoot to the same XamlRoot as an element that is already present in the AppWindow.
if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8))
{
contentDialog.XamlRoot = this.Content.XamlRoot;
}
ContentDialogResult contentDialogResult = await contentDialog.ShowAsync();
switch (contentDialogResult)
{
case ContentDialogResult.Primary:
// Save, then continue the merge action
if (!AskFileName(string.Empty) ||
!SaveFile())
{
return;
}
break;
case ContentDialogResult.Secondary:
// Don't save and continue the merge action!
UpdateUnsavedFileState(false);
break;
default:
// Don't merge the file!
return;
}
}
// pass in the filename so we can edit the current file
OpenRegistryEditor(_appFileName);
}
///
/// Opens the currently saved file in the PC's default REG file editor (often Notepad)
///
private void EditButton_Click(object sender, RoutedEventArgs e)
{
// use the REG file's filename and verb so we can respect the selected editor
Process process = new Process();
process.StartInfo.FileName = string.Format(CultureInfo.InvariantCulture, "\"{0}\"", _appFileName);
process.StartInfo.Verb = "Edit";
process.StartInfo.UseShellExecute = true;
try
{
process.Start();
}
catch
{
ShowMessageBox(
resourceLoader.GetString("ErrorDialogTitle"),
resourceLoader.GetString("FileEditorError"),
resourceLoader.GetString("OkButtonText"));
}
}
///
/// Trigger that fires when a node in treeView is clicked and which populates dataGrid
/// Can also be fired from elsewhere in the code
///
private void TreeView_ItemInvoked(TreeView sender, TreeViewItemInvokedEventArgs args)
{
TreeViewItemInvokedEventArgs localArgs = args as TreeViewItemInvokedEventArgs;
TreeViewNode treeViewNode = null;
// if there are no args, the mouse didn't get clicked but we want to believe it did
if (args != null)
{
treeViewNode = args.InvokedItem as TreeViewNode;
}
else
{
treeViewNode = treeView.SelectedNode;
}
// Update the toolbar button for the tree
registryJumpToKeyButton.IsEnabled = CheckTreeForValidKey();
// Grab the object that has Registry data in it from the currently selected treeView node
RegistryKey registryKey = (RegistryKey)treeViewNode.Content;
// no matter what happens, clear the ListView of items on each click
ClearTable();
// if there's no ListView items stored for the selected node, dataGrid is clear so get out now
if (registryKey.Tag == null)
{
return;
}
// if there WAS something in the Tag property, cast it to a list and Populate the ListView
ArrayList arrayList = (ArrayList)registryKey.Tag;
listRegistryValues = new List();
for (int i = 0; i < arrayList.Count; i++)
{
RegistryValue listViewItem = (RegistryValue)arrayList[i];
listRegistryValues.Add(listViewItem);
}
// create a new binding for dataGrid and reattach it, updating the rows
Binding listRegistryValuesBinding = new Binding { Source = listRegistryValues };
dataGrid.SetBinding(DataGrid.ItemsSourceProperty, listRegistryValuesBinding);
}
///
/// When the text in editor changes, reload treeView and possibly dataGrid and reset the save button
///
private void MonacoEditor_TextChanged(object sender, EventArgs e)
{
_dispatcherQueue.TryEnqueue(() =>
{
RefreshRegistryFile();
if (!editorContentChangedScripted)
{
UpdateUnsavedFileState(true);
}
editorContentChangedScripted = false;
});
}
///
/// Sets indicator for programatic text change and adds text changed handler
///
///
/// Use this always, if button actions temporary disable the text changed event
///
private void ButtonAction_RestoreTextChangedEvent()
{
// Solves the problem that enabling the event handler fires it one time.
// These one time fired event would causes wrong unsaved changes state.
editorContentChangedScripted = true;
MonacoEditor.TextChanged += MonacoEditor_TextChanged;
}
// Commands to show data preview
public void ButtonExtendedPreview_Click(object sender, RoutedEventArgs e)
{
var data = ((Button)sender).DataContext as RegistryValue;
InvokeExtendedDataPreview(data);
}
public void MenuExtendedPreview_Click(object sender, RoutedEventArgs e)
{
var data = ((MenuFlyoutItem)sender).DataContext as RegistryValue;
InvokeExtendedDataPreview(data);
}
private async void InvokeExtendedDataPreview(RegistryValue valueData)
{
// Only one content dialog can be open at the same time and multiple instances of data preview can crash the app.
if (_dialogSemaphore.CurrentCount == 0)
{
return;
}
try
{
// Lock ui and request dialog lock
_dialogSemaphore.Wait();
ChangeCursor(gridPreview, true);
await ShowExtendedDataPreview(valueData.Name, valueData.Type, valueData.Value);
}
catch
{
#if DEBUG
throw;
#endif
}
finally
{
// Unblock ui and release dialog lock
ChangeCursor(gridPreview, false);
_dialogSemaphore.Release();
}
}
}
}