5 Commits

Author SHA1 Message Date
3c6cc15281 Check for updates when tray menu opens
All checks were successful
Build / build (push) Successful in 9h0m7s
Build and Release / build (push) Successful in 8h0m11s
- Checks for updates in background when user clicks tray icon
- Shows clickable blue banner when update available
- Rate limited to once per hour to avoid spam
- Version 1.1.5

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 02:56:53 -05:00
ce3402f1a9 Add About dialog with version info
All checks were successful
Build / build (push) Successful in 9h0m7s
Build and Release / build (push) Successful in 8h0m11s
- About button added next to Exit in header
- Shows app name, version, author, and company
- Version 1.1.4

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 02:51:12 -05:00
0e530238f6 Ship ControlMyMonitor.exe alongside instead of embedding
All checks were successful
Build / build (push) Successful in 9h0m7s
Build and Release / build (push) Successful in 8h0m11s
- Remove embedded resource pattern (dropper behavior triggers AV)
- ControlMyMonitor.exe now copied to output directory as content
- Removes extraction to temp folder at runtime
- Should eliminate Wacatac.H!ml false positive

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 02:47:54 -05:00
fc3ebe14be Add Reset and Detect buttons to Config dialog
All checks were successful
Build / build (push) Successful in 9h0m7s
Build and Release / build (push) Successful in 9h0m12s
- Reset button: clears custom labels, unhides all ports, removes discovered ports
- Detect button: tries common VCP 60 values to discover available input ports
- Version bumped to 1.1.2

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 02:41:42 -05:00
0c860d19ea Remove batch file creation to reduce AV false positives
All checks were successful
Build / build (push) Successful in 9h0m10s
Build and Release / build (push) Successful in 9h0m12s
- Refactored all process execution to use direct exe calls
- Added ExecuteExeAsync method for direct process execution
- Removed dynamic .bat file creation that triggered Wacatac detection
- All commands now run ControlMyMonitor.exe directly with arguments

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 02:29:16 -05:00
10 changed files with 404 additions and 68 deletions

View File

@@ -98,12 +98,20 @@
<!-- Footer Buttons -->
<Border Grid.Row="2" Background="#3A3A3A" CornerRadius="0,0,8,8" Padding="12,10">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="Cancel" Width="80" Margin="0,0,8,0" Click="CancelButton_Click"
Style="{StaticResource DarkButton}"/>
<Button Content="Save" Width="80" Click="SaveButton_Click"
Style="{StaticResource PrimaryButton}"/>
</StackPanel>
<Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<Button Content="Reset" Width="70" Margin="0,0,8,0" Click="ResetButton_Click"
Style="{StaticResource DarkButton}" ToolTip="Clear all custom labels and unhide all ports"/>
<Button Name="btnDetect" Content="Detect" Width="70" Click="DetectButton_Click"
Style="{StaticResource DarkButton}" ToolTip="Try to detect available input ports"/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="Cancel" Width="80" Margin="0,0,8,0" Click="CancelButton_Click"
Style="{StaticResource DarkButton}"/>
<Button Content="Save" Width="80" Click="SaveButton_Click"
Style="{StaticResource PrimaryButton}"/>
</StackPanel>
</Grid>
</Border>
</Grid>
</Border>

View File

@@ -1,7 +1,10 @@
using CMM.Library.Config;
using CMM.Library.Method;
using CMM.Library.ViewModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
@@ -180,6 +183,107 @@ public partial class ConfigWindow : Window
Close();
}
private void ResetButton_Click(object sender, RoutedEventArgs e)
{
// Reset all port rows to default values
foreach (var row in _portRows)
{
row.CustomLabel = "";
row.IsHidden = false;
row.ShowInQuickSwitch = false;
}
// Reload the UI to reflect changes
LoadPortConfiguration();
// Clear any discovered ports from config
var config = MonitorConfigManager.GetMonitorConfig(_serialNumber);
config.Ports.Clear();
MonitorConfigManager.SaveMonitorConfig(config);
MonitorConfigManager.ClearCache();
}
private async void DetectButton_Click(object sender, RoutedEventArgs e)
{
btnDetect.IsEnabled = false;
btnDetect.Content = "...";
try
{
// Common VCP 60 values: 15=DP1, 16=DP2, 17=HDMI1, 18=HDMI2, 3=DVI1, 4=DVI2, 1=VGA1, 2=VGA2
var commonPorts = new[] { 15, 16, 17, 18, 3, 4, 1, 2 };
var detectedPorts = new List<int>();
// Get current input so we can restore it
var currentInput = await CMMCommand.GetInputSource(_serialNumber);
foreach (var vcpValue in commonPorts)
{
// Skip ports we already know about
if (_availablePorts.Any(p => p.Value == vcpValue))
continue;
try
{
// Try to set the input - if it succeeds, the port exists
await CMMCommand.SetInputSource(_serialNumber, vcpValue);
await Task.Delay(500); // Give monitor time to respond
// Check if the input actually changed
var newInput = await CMMCommand.GetInputSource(_serialNumber);
if (newInput == vcpValue)
{
detectedPorts.Add(vcpValue);
}
}
catch
{
// Port doesn't exist or isn't supported
}
}
// Restore original input if we have one
if (currentInput.HasValue)
{
await CMMCommand.SetInputSource(_serialNumber, currentInput.Value);
}
if (detectedPorts.Count > 0)
{
// Add detected ports to the available list and config
foreach (var vcpValue in detectedPorts)
{
var name = CMMCommand.GetInputSourceName(vcpValue);
_availablePorts.Add(new InputSourceOption(vcpValue, name));
MonitorConfigManager.AddDiscoveredPort(_serialNumber, _monitorName, vcpValue, name);
}
// Reload to show new ports
MonitorConfigManager.ClearCache();
LoadPortConfiguration();
MessageBox.Show($"Detected {detectedPorts.Count} new port(s):\n" +
string.Join("\n", detectedPorts.Select(v => $" • {CMMCommand.GetInputSourceName(v)}")),
"Detection Complete", MessageBoxButton.OK, MessageBoxImage.Information);
}
else
{
MessageBox.Show("No additional ports were detected.", "Detection Complete",
MessageBoxButton.OK, MessageBoxImage.Information);
}
}
catch (Exception ex)
{
MessageBox.Show($"Detection failed: {ex.Message}", "Error",
MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
btnDetect.Content = "Detect";
btnDetect.IsEnabled = true;
}
}
private class PortConfigRow
{
public int VcpValue { get; set; }

View File

@@ -15,9 +15,9 @@
</PropertyGroup>
<PropertyGroup>
<Version>1.1.0</Version>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<Version>1.1.5</Version>
<AssemblyVersion>1.1.5.0</AssemblyVersion>
<FileVersion>1.1.5.0</FileVersion>
</PropertyGroup>
<ItemGroup>

View File

@@ -187,9 +187,26 @@
<!-- Header -->
<Border Grid.Row="0" Background="#444" CornerRadius="8,8,0,0" Padding="12,10">
<Grid>
<TextBlock Text="Monitor Control" Foreground="White" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center"/>
<Button Content="Exit" HorizontalAlignment="Right" Click="ExitButton_Click"
Style="{StaticResource DarkButton}" FontSize="11"/>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<TextBlock Text="Monitor Control" Foreground="White" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="About" Margin="0,0,6,0" Click="AboutButton_Click"
Style="{StaticResource DarkButton}" FontSize="11"/>
<Button Content="Exit" Click="ExitButton_Click"
Style="{StaticResource DarkButton}" FontSize="11"/>
</StackPanel>
</Grid>
<!-- Update Banner -->
<Border Name="updateBanner" Grid.Row="1" Background="#0078D4" CornerRadius="3"
Padding="8,4" Margin="0,8,0,0" Visibility="Collapsed" Cursor="Hand"
MouseLeftButtonUp="UpdateBanner_Click">
<TextBlock Name="updateText" Text="Update available!" Foreground="White"
FontSize="11" HorizontalAlignment="Center"/>
</Border>
</Grid>
</Border>

View File

@@ -18,6 +18,8 @@ public partial class MainWindow : Window
private List<(XMonitor Monitor, List<InputSourceOption> Options)> _loadedMonitors = new();
private Storyboard? _spinnerStoryboard;
private DispatcherTimer? _showLogButtonTimer;
private UpdateInfo? _pendingUpdate;
private DateTime _lastUpdateCheck = DateTime.MinValue;
public MainWindow()
{
@@ -53,12 +55,50 @@ public partial class MainWindow : Window
};
_showLogButtonTimer.Start();
// Check for updates in background (max once per hour)
if ((DateTime.Now - _lastUpdateCheck).TotalMinutes > 60)
{
_lastUpdateCheck = DateTime.Now;
_ = CheckForUpdatesAsync();
}
Dispatcher.BeginInvoke(new Action(() =>
{
Top = workArea.Bottom - ActualHeight - 10;
}), DispatcherPriority.Loaded);
}
private async Task CheckForUpdatesAsync()
{
try
{
var update = await UpdateChecker.CheckForUpdateAsync();
if (update != null)
{
_pendingUpdate = update;
await Dispatcher.InvokeAsync(() => ShowUpdateBanner(update));
}
}
catch
{
// Silently ignore update check failures
}
}
private void ShowUpdateBanner(UpdateInfo update)
{
updateBanner.Visibility = Visibility.Visible;
updateText.Text = $"v{update.LatestVersion} available - Click to update";
}
private void UpdateBanner_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (_pendingUpdate != null && !string.IsNullOrEmpty(_pendingUpdate.DownloadUrl))
{
UpdateChecker.OpenDownloadPage(_pendingUpdate.DownloadUrl);
}
}
private void Window_Deactivated(object sender, EventArgs e)
{
Hide();
@@ -70,6 +110,85 @@ public partial class MainWindow : Window
Application.Current.Shutdown();
}
private void AboutButton_Click(object sender, RoutedEventArgs e)
{
var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
var versionStr = $"{version?.Major}.{version?.Minor}.{version?.Build}";
var aboutWindow = new Window
{
Title = "About Monitor Control",
Width = 300,
Height = 180,
WindowStartupLocation = WindowStartupLocation.CenterScreen,
ResizeMode = ResizeMode.NoResize,
WindowStyle = WindowStyle.None,
AllowsTransparency = true,
Background = Brushes.Transparent
};
var border = new Border
{
Background = new SolidColorBrush(Color.FromArgb(0xF0, 0x33, 0x33, 0x33)),
CornerRadius = new CornerRadius(8),
BorderBrush = new SolidColorBrush(Color.FromRgb(0x55, 0x55, 0x55)),
BorderThickness = new Thickness(1),
Padding = new Thickness(20)
};
var stack = new StackPanel { VerticalAlignment = VerticalAlignment.Center };
stack.Children.Add(new TextBlock
{
Text = "Monitor Control",
Foreground = Brushes.White,
FontSize = 18,
FontWeight = FontWeights.SemiBold,
HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(0, 0, 0, 5)
});
stack.Children.Add(new TextBlock
{
Text = $"Version {versionStr}",
Foreground = Brushes.LightGray,
FontSize = 12,
HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(0, 0, 0, 10)
});
stack.Children.Add(new TextBlock
{
Text = "by David H. Friedel Jr",
Foreground = Brushes.Gray,
FontSize = 11,
HorizontalAlignment = HorizontalAlignment.Center
});
stack.Children.Add(new TextBlock
{
Text = "MarketAlly",
Foreground = Brushes.Gray,
FontSize = 11,
HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(0, 0, 0, 15)
});
var closeBtn = new Button
{
Content = "OK",
Width = 80,
HorizontalAlignment = HorizontalAlignment.Center,
Style = (Style)FindResource("DarkButton")
};
closeBtn.Click += (s, args) => aboutWindow.Close();
stack.Children.Add(closeBtn);
border.Child = stack;
aboutWindow.Content = border;
aboutWindow.ShowDialog();
}
private void ShowLogButton_Click(object sender, RoutedEventArgs e)
{
var logWindow = new Window
@@ -181,13 +300,25 @@ public partial class MainWindow : Window
var inputOptions = await CMMCommand.GetInputSourceOptions(m.SerialNumber);
DebugLogger.Log($" Input options count: {inputOptions.Count}");
// Add any previously discovered ports from config
var discoveredPorts = MonitorConfigManager.GetDiscoveredPorts(m.SerialNumber, inputOptions);
foreach (var port in discoveredPorts)
{
inputOptions.Insert(0, port);
DebugLogger.Log($" Added discovered port from config: {port.Value} ({port.Name})");
}
// Some monitors don't report current input in their possible values list
// Add it if missing so user can see what's currently selected
// Add it if missing and save to config for future
if (inputSource.HasValue && !inputOptions.Any(o => o.Value == inputSource.Value))
{
var currentInputName = CMMCommand.GetInputSourceName(inputSource.Value);
inputOptions.Insert(0, new InputSourceOption(inputSource.Value, currentInputName));
DebugLogger.Log($" Added missing current input: {inputSource.Value} ({currentInputName})");
// Save this discovered port so it appears in future even when not current
MonitorConfigManager.AddDiscoveredPort(m.SerialNumber, m.MonitorName, inputSource.Value, currentInputName);
DebugLogger.Log($" Saved discovered port to config");
}
DebugLogger.Log($" Getting power status...");

View File

@@ -1,5 +1,5 @@
#define MyAppName "Monitor Control"
#define MyAppVersion "1.1.0"
#define MyAppVersion "1.1.5"
#define MyAppPublisher "MarketAlly"
#define MyAppExeName "DellMonitorControl.exe"
#define MyAppIcon "MonitorIcon.ico"

View File

@@ -126,4 +126,57 @@ public static class MonitorConfigManager
{
_cachedConfig = null;
}
/// <summary>
/// Save a discovered port that the monitor didn't report in its possible values.
/// This ensures ports like DisplayPort-1 are remembered even if the monitor firmware doesn't list them.
/// </summary>
public static void AddDiscoveredPort(string serialNumber, string monitorName, int vcpValue, string defaultName)
{
var config = Load();
var monitorConfig = config.Monitors.FirstOrDefault(m => m.SerialNumber == serialNumber);
if (monitorConfig == null)
{
monitorConfig = new MonitorConfig { SerialNumber = serialNumber, MonitorName = monitorName };
config.Monitors.Add(monitorConfig);
}
// Check if port already exists
if (monitorConfig.Ports.Any(p => p.VcpValue == vcpValue))
return;
// Add the discovered port
monitorConfig.Ports.Add(new PortConfig
{
VcpValue = vcpValue,
DefaultName = defaultName,
CustomLabel = "",
IsHidden = false,
ShowInQuickSwitch = false
});
Save(config);
}
/// <summary>
/// Get any discovered ports from config that the monitor didn't report.
/// </summary>
public static List<InputSourceOption> GetDiscoveredPorts(string serialNumber, List<InputSourceOption> reportedOptions)
{
var discovered = new List<InputSourceOption>();
var monitorConfig = GetMonitorConfig(serialNumber);
foreach (var port in monitorConfig.Ports)
{
// If this port isn't in the reported options, it's a discovered port
if (!reportedOptions.Any(o => o.Value == port.VcpValue))
{
var name = !string.IsNullOrWhiteSpace(port.CustomLabel) ? port.CustomLabel : port.DefaultName;
discovered.Add(new InputSourceOption(port.VcpValue, name));
}
}
return discovered;
}
}

View File

@@ -41,6 +41,46 @@ internal class ConsoleHelper
return await readTask;
}
/// <summary>
/// Execute an exe directly with arguments (no cmd.exe or batch file wrapper)
/// </summary>
public static async Task<(string Output, int ExitCode)> ExecuteExeAsync(string exePath, string arguments, int timeoutMs = 5000)
{
var p = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = exePath,
Arguments = arguments,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
}
};
p.Start();
var readTask = Task.Run(async () =>
{
var output = await p.StandardOutput.ReadToEndAsync();
await p.WaitForExitAsync();
return (output, p.ExitCode);
});
var completedTask = await Task.WhenAny(readTask, Task.Delay(timeoutMs));
if (completedTask != readTask)
{
try { p.Kill(true); } catch { }
return (string.Empty, -1);
}
var result = await readTask;
p.Close();
return result;
}
public static async Task<string> CmdCommandAsync(params string[] cmds) =>
await CommandAsync(cmdFileName, cmds);

View File

@@ -25,13 +25,10 @@
</PropertyGroup>
<ItemGroup>
<None Remove="Resource\ControlMyMonitor.exe" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resource\ControlMyMonitor.exe">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<Content Include="Resource\ControlMyMonitor.exe">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>ControlMyMonitor.exe</Link>
</Content>
</ItemGroup>
<ItemGroup>

View File

@@ -10,47 +10,51 @@ namespace CMM.Library.Method;
/// </summary>
public static class CMMCommand
{
static readonly string CMMTmpFolder = Path.Combine(Path.GetTempPath(), $"CMM");
static readonly string CMMexe = Path.Combine(CMMTmpFolder, "ControlMyMonitor.exe");
static readonly string CMMTmpFolder = Path.Combine(Path.GetTempPath(), "CMM");
static readonly string CMMexe = Path.Combine(AppContext.BaseDirectory, "ControlMyMonitor.exe");
static readonly string CMMsMonitors = Path.Combine(CMMTmpFolder, "smonitors.tmp");
public static async Task ScanMonitor()
{
await BytesToFileAsync(new(CMMexe));
await ConsoleHelper.CmdCommandAsync($"{CMMexe} /smonitors {CMMsMonitors}");
// Ensure temp folder exists for output files
Directory.CreateDirectory(CMMTmpFolder);
await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/smonitors {CMMsMonitors}");
}
public static Task PowerOn(string monitorSN)
public static async Task PowerOn(string monitorSN)
{
return ConsoleHelper.CmdCommandAsync($"{CMMexe} /SetValue {monitorSN} D6 1");
await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/SetValue {monitorSN} D6 1");
}
public static Task Sleep(string monitorSN)
public static async Task Sleep(string monitorSN)
{
return ConsoleHelper.CmdCommandAsync($"{CMMexe} /SetValue {monitorSN} D6 4");
await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/SetValue {monitorSN} D6 4");
}
private static async Task<string> GetMonitorValue(string monitorSN, string vcpCode = "D6", int maxRetries = 2)
{
for (int attempt = 0; attempt <= maxRetries; attempt++)
{
var cmdFileName = Path.Combine(CMMTmpFolder, $"{Guid.NewGuid()}.bat");
var cmd = $"{CMMexe} /GetValue {monitorSN} {vcpCode}\r\n" +
$"echo %errorlevel%";
File.WriteAllText(cmdFileName, cmd);
var values = await ConsoleHelper.ExecuteCommand(cmdFileName);
try { File.Delete(cmdFileName); } catch { }
// Execute directly without batch file wrapper
var (output, exitCode) = await ConsoleHelper.ExecuteExeAsync(
CMMexe,
$"/GetValue {monitorSN} {vcpCode}");
// Empty result means timeout - don't retry, monitor is unresponsive
if (string.IsNullOrEmpty(values))
// Timeout
if (exitCode == -1)
return string.Empty;
var value = values.Split("\r\n", StringSplitOptions.RemoveEmptyEntries).LastOrDefault();
// ControlMyMonitor returns the value as the exit code
// Exit code > 0 means success with that value
if (exitCode > 0)
return exitCode.ToString();
// Also check stdout in case it outputs there
var value = output?.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault()?.Trim();
if (!string.IsNullOrEmpty(value) && value != "0")
return value;
// Only retry on non-timeout failures
// Only retry on failure
if (attempt < maxRetries)
await Task.Delay(300);
}
@@ -60,9 +64,9 @@ public static class CMMCommand
#region Brightness (VCP Code 10)
public static Task SetBrightness(string monitorSN, int value)
public static async Task SetBrightness(string monitorSN, int value)
{
return ConsoleHelper.CmdCommandAsync($"{CMMexe} /SetValue {monitorSN} 10 {value}");
await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/SetValue {monitorSN} 10 {value}");
}
public static async Task<int?> GetBrightness(string monitorSN)
@@ -75,9 +79,9 @@ public static class CMMCommand
#region Contrast (VCP Code 12)
public static Task SetContrast(string monitorSN, int value)
public static async Task SetContrast(string monitorSN, int value)
{
return ConsoleHelper.CmdCommandAsync($"{CMMexe} /SetValue {monitorSN} 12 {value}");
await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/SetValue {monitorSN} 12 {value}");
}
public static async Task<int?> GetContrast(string monitorSN)
@@ -90,9 +94,9 @@ public static class CMMCommand
#region Input Source (VCP Code 60)
public static Task SetInputSource(string monitorSN, int value)
public static async Task SetInputSource(string monitorSN, int value)
{
return ConsoleHelper.CmdCommandAsync($"{CMMexe} /SetValue {monitorSN} 60 {value}");
await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/SetValue {monitorSN} 60 {value}");
}
public static async Task<int?> GetInputSource(string monitorSN)
@@ -106,7 +110,7 @@ public static class CMMCommand
var options = new List<InputSourceOption>();
var savePath = Path.Combine(CMMTmpFolder, $"{monitorSN}_vcp.tmp");
await ConsoleHelper.CmdCommandAsync($"{CMMexe} /sjson {savePath} {monitorSN}");
await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/sjson {savePath} {monitorSN}");
if (!File.Exists(savePath)) return options;
@@ -183,7 +187,7 @@ public static class CMMCommand
static async Task ScanMonitorStatus(string savePath, XMonitor mon)
{
await ConsoleHelper.CmdCommandAsync($"{CMMexe} /sjson {savePath} {mon.MonitorID}");
await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/sjson {savePath} {mon.MonitorID}");
var monitorModel = JsonHelper.JsonFormFile<IEnumerable<SMonitorModel>>(savePath);
var status = monitorModel.ReadMonitorStatus();
@@ -276,22 +280,4 @@ public static class CMMCommand
return monitors;
}
static void BytesToFile(FileInfo fi)
{
fi.Refresh();
if (fi.Exists) return;
if (!fi.Directory.Exists) fi.Directory.Create();
File.WriteAllBytes(fi.FullName, fi.Name.ResourceToByteArray());
}
static async Task BytesToFileAsync(FileInfo fi)
{
fi.Refresh();
if (fi.Exists) return;
if (!fi.Directory.Exists) fi.Directory.Create();
await File.WriteAllBytesAsync(fi.FullName, fi.Name.ResourceToByteArray());
}
}