commit bfde26dee564b18d714f5a15c78edb708c5a8ef4 Author: 최준영 Date: Mon Apr 1 17:28:41 2024 +0900 [build] 토이 프로젝트 작업 변경 1. .gitignore 파일이 추가되어, 컴파일된 바이너리, 로그, 임시 파일 등이 Git 추적에서 제외되었습니다. 2. WPF의 MVVM 패턴을 따르는 여러 뷰(LoginWindow.xaml, MainWindow.xaml, RegistMemberWindow.xaml), 뷰 모델(LoginWindowViewModel.cs, MainWindowViewModel.cs, RegistMemberWindowViewModel.cs), 그리고 모델(Member.cs)이 추가되었습니다. 3. 리소스 및 스타일 정의를 포함한 XAML 리소스 파일(DefaultTheme.xaml, LightTheme.xaml, Buttons.xaml 등)이 추가되어 UI의 모양과 느낌을 커스터마이즈할 수 있게 되었습니다. 4. 애플리케이션 설정(AppSettings.cs)과 관련된 설정 파일과 로그인 정보(LoginInfo.cs)를 처리하는 코드가 추가되었습니다. 5. 이미지 리소스(bg_login.jpeg, btn_search_001.png, ico_pw.png, ico_user.png)가 추가되어 UI에 사용됩니다. 6. 유틸리티 및 서비스(ResourceExplorer.cs, ViewModelLocator.cs 등)를 처리하는 여러 보조 클래스들이 추가되었습니다. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..33c239e --- /dev/null +++ b/.gitignore @@ -0,0 +1,498 @@ +# ---> C++ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# ---> Node +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# ---> Java +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]bjL/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +*_i.c +*_p.c +*_i.h +*_h.h +*.meta +*.obj +*.pch +*.pgc +*.pgd +*.rsp +*.sbr +*.tli +*.tlh +*.tlb +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc +*.tlog +*.metagen + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Typescript v1 declaration files +typings/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs \ No newline at end of file diff --git a/src/WPFEduSolution/WPFBeginner/App.xaml b/src/WPFEduSolution/WPFBeginner/App.xaml new file mode 100644 index 0000000..502643e --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/App.xaml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + diff --git a/src/WPFEduSolution/WPFBeginner/App.xaml.cs b/src/WPFEduSolution/WPFBeginner/App.xaml.cs new file mode 100644 index 0000000..6e9bd5b --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/App.xaml.cs @@ -0,0 +1,109 @@ +using System; +using System.Globalization; +using System.IO; +using System.Text.Json; +using System.Windows; +using CommunityToolkit.Mvvm.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; +using WPFBeginner.Configurations; +using WPFBeginner.Exceptions; +using WPFBeginner.Services; +using WPFBeginner.ViewModels; +using WPFBeginner.Views; + +namespace WPFBeginner +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + private readonly static Dictionary _languages = new Dictionary(); + private static ResourceDictionary _currentLanguage; + + private readonly static Dictionary _themes = new Dictionary(); + private static ResourceDictionary _currentTheme; + + public static AppSettings Config { get; private set; } + + //[STAThread] + //static void Main() + //{ + //try + //{ + // var application = new App(); + // application.InitializeComponent(); // For resouce load. + // application.Run(); + //} + //catch (AlreadyExcutedAppException ex) + //{ + // MessageBox.Show("이미 실행중입니다. 종료 후, 다시 실행해주세요"); + //} + //catch (LoginFailedException ex) + //{ + // // Do not anything; + //} + //catch (Exception ex) + //{ + // // log + //} + //} + + protected override void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + + //Login(); + InitializeService(); + } + + private void InitializeService() + { + Config = JsonSerializer.Deserialize(File.ReadAllText("appsettings.json")); + + _languages.Add("ko-KR", ResourceExplorer.GetLanguageResourceDic("ko-KR")); + _languages.Add("en-US", ResourceExplorer.GetLanguageResourceDic("en-US")); + SetLanguage(Config.Language); + + _themes.Add("DefaultTheme", ResourceExplorer.GetThemeResourceDic("DefaultTheme")); + _themes.Add("LightTheme", ResourceExplorer.GetThemeResourceDic("LightTheme")); + SetTheme(Config.Theme); + + Config.LanguageChanged += (s, e) => SetLanguage(e.New); + Config.ThemeChanged += (s, e) => SetTheme(e.New); + + Ioc.Default.ConfigureServices( + new ServiceCollection() + .AddSingleton(new MainWindowViewModel()) + .BuildServiceProvider()); + } + + private void SetTheme(string code) + { + if (_currentTheme != null) + Resources.MergedDictionaries.Remove(_currentTheme); + + _currentTheme = ResourceExplorer.GetThemeResourceDic(code); + Resources.MergedDictionaries.Add(_currentTheme); + } + + private void SetLanguage(string code) + { + if (_currentLanguage != null) + Resources.MergedDictionaries.Remove( _currentLanguage ); + + _currentLanguage = ResourceExplorer.GetLanguageResourceDic(code); + Resources.MergedDictionaries.Add(_currentLanguage); + } + + private static void Login() + { + var login = new LoginWindow(); + var isSuccess = login.ShowDialog(); + + if (isSuccess != true) + throw new LoginFailedException(); + } + } + +} diff --git a/src/WPFEduSolution/WPFBeginner/AssemblyInfo.cs b/src/WPFEduSolution/WPFBeginner/AssemblyInfo.cs new file mode 100644 index 0000000..b0ec827 --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/src/WPFEduSolution/WPFBeginner/Behaviors/WindowBehavior.cs b/src/WPFEduSolution/WPFBeginner/Behaviors/WindowBehavior.cs new file mode 100644 index 0000000..366ab09 --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/Behaviors/WindowBehavior.cs @@ -0,0 +1,277 @@ +using System.Runtime.InteropServices; +using System.Windows.Interop; +using System.Windows; + +namespace WPFBeginner.Behaviors +{ + public class WindowBehavior + { + #region Win32 imports + + private const int GWL_STYLE = -16; + private const int WS_SYSMENU = 0x80000; + private const int WS_MAXIMIZEBOX = 0x80000; + private const int WS_MINIMIZEBOX = 0x80000; + + [DllImport("user32.dll", SetLastError = true)] + private static extern int GetWindowLong(IntPtr hWnd, int nIndex); + [DllImport("user32.dll")] + private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); + + #endregion + + private static readonly Type OwnerType = typeof(WindowBehavior); + + #region HideCloseButton (attached property) + + private static readonly RoutedEventHandler HideCloseButtonWhenLoadedDelegate = (sender, args) => { + if (sender is Window == false) + return; + + var w = (Window)sender; + HideCloseButton(w); + w.Loaded -= HideCloseButtonWhenLoadedDelegate; + }; + + private static readonly RoutedEventHandler ShowCloseButtonWhenLoadedDelegate = (sender, args) => { + if (sender is Window == false) + return; + + var w = (Window)sender; + ShowCloseButton(w); + w.Loaded -= ShowCloseButtonWhenLoadedDelegate; + }; + + public static readonly DependencyProperty HideCloseButtonProperty = + DependencyProperty.RegisterAttached("HideCloseButton", typeof(bool), OwnerType, + new FrameworkPropertyMetadata(false, new PropertyChangedCallback(HideCloseButtonChangedCallback))); + + [AttachedPropertyBrowsableForType(typeof(Window))] + public static bool GetHideCloseButton(Window obj) => (bool)obj.GetValue(HideCloseButtonProperty); + + [AttachedPropertyBrowsableForType(typeof(Window))] + public static void SetHideCloseButton(Window obj, bool value) => obj.SetValue(HideCloseButtonProperty, value); + + private static void HideCloseButtonChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var window = d as Window; + if (window == null) return; + + var hideCloseButton = (bool)e.NewValue; + if (hideCloseButton && !GetIsHiddenCloseButton(window)) + { + if (!window.IsLoaded) + window.Loaded += HideCloseButtonWhenLoadedDelegate; + else + HideCloseButton(window); + + SetIsHiddenCloseButton(window, true); + } + else if (!hideCloseButton && GetIsHiddenCloseButton(window)) + { + if (!window.IsLoaded) + window.Loaded -= ShowCloseButtonWhenLoadedDelegate; + else + ShowCloseButton(window); + + SetIsHiddenCloseButton(window, false); + } + } + + + + private static void HideCloseButton(Window w) + { + var hwnd = new WindowInteropHelper(w).Handle; + SetWindowLong(hwnd, GWL_STYLE, GetWindowLong(hwnd, GWL_STYLE) & ~WS_SYSMENU); + } + + private static void ShowCloseButton(Window w) + { + var hwnd = new WindowInteropHelper(w).Handle; + SetWindowLong(hwnd, GWL_STYLE, GetWindowLong(hwnd, GWL_STYLE) | WS_SYSMENU); + } + + #endregion + + #region IsHiddenCloseButton (readonly attached property) + + private static readonly DependencyPropertyKey IsHiddenCloseButtonKey = + DependencyProperty.RegisterAttachedReadOnly( "IsHiddenCloseButton", typeof(bool), OwnerType, new FrameworkPropertyMetadata(false)); + + public static readonly DependencyProperty IsHiddenCloseButtonProperty = IsHiddenCloseButtonKey.DependencyProperty; + + [AttachedPropertyBrowsableForType(typeof(Window))] + public static bool GetIsHiddenCloseButton(Window obj) => (bool)obj.GetValue(IsHiddenCloseButtonProperty); + + private static void SetIsHiddenCloseButton(Window obj, bool value) => obj.SetValue(IsHiddenCloseButtonKey, value); + + #endregion + + //#region HideMinimizeButton + + //private static readonly RoutedEventHandler HideMinimizeButtonWhenLoadedDelegate = (sender, args) => { + // if (sender is Window == false) + // return; + + // var w = (Window)sender; + // HideMinimizeButton(w); + // w.Loaded -= HideMinimizeButtonWhenLoadedDelegate; + //}; + + //private static readonly RoutedEventHandler ShowMinimizeButtonWhenLoadedDelegate = (sender, args) => { + // if (sender is Window == false) + // return; + + // var w = (Window)sender; + // ShowMinimizeButton(w); + // w.Loaded -= ShowMinimizeButtonWhenLoadedDelegate; + //}; + + //public static readonly DependencyProperty HideMinimizeButtonProperty = + // DependencyProperty.RegisterAttached("HideMinimizeButton", typeof(bool), OwnerType, + // new FrameworkPropertyMetadata(false, new PropertyChangedCallback(HideMinimizeButtonChangedCallback))); + + //[AttachedPropertyBrowsableForType(typeof(Window))] + //public static bool GetHideMinimizeButton(Window obj) => (bool)obj.GetValue(HideCloseButtonProperty); + + //[AttachedPropertyBrowsableForType(typeof(Window))] + //public static void SetHideMinimizeButton(Window obj, bool value) => obj.SetValue(HideCloseButtonProperty, value); + + //private static void HideMinimizeButtonChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) + //{ + // var window = d as Window; + // if (window == null) return; + + // var hideCloseButton = (bool)e.NewValue; + // if (hideCloseButton && !GetIsHiddenMinimizeButton(window)) + // { + // if (!window.IsLoaded) + // window.Loaded += HideMinimizeButtonWhenLoadedDelegate; + // else + // HideMinimizeButton(window); + + // SetIsHiddenMinimizeButton(window, true); + // } + // else if (!hideCloseButton && GetIsHiddenMinimizeButton(window)) + // { + // if (!window.IsLoaded) + // window.Loaded -= ShowMinimizeButtonWhenLoadedDelegate; + // else + // ShowMinimizeButton(window); + + // SetIsHiddenMinimizeButton(window, false); + // } + //} + + //private static void HideMinimizeButton(Window w) + //{ + // var hwnd = new WindowInteropHelper(w).Handle; + // SetWindowLong(hwnd, GWL_STYLE, GetWindowLong(hwnd, GWL_STYLE) & ~WS_MINIMIZEBOX); + //} + + //private static void ShowMinimizeButton(Window w) + //{ + // var hwnd = new WindowInteropHelper(w).Handle; + // SetWindowLong(hwnd, GWL_STYLE, GetWindowLong(hwnd, GWL_STYLE) | WS_MINIMIZEBOX); + //} + + //#endregion + + //#region IsHiddenMinimizeButton (readonly attached property) + //private static readonly DependencyPropertyKey IsHiddenMinimizeButtonKey = + // DependencyProperty.RegisterAttachedReadOnly("IsHiddenMinimizeButton", typeof(bool), OwnerType, new FrameworkPropertyMetadata(false)); + + //public static readonly DependencyProperty IsHiddenMinimizeButtonProperty = IsHiddenMinimizeButtonKey.DependencyProperty; + + //[AttachedPropertyBrowsableForType(typeof(Window))] + //public static bool GetIsHiddenMinimizeButton(Window obj) => (bool)obj.GetValue(IsHiddenMinimizeButtonProperty); + + //private static void SetIsHiddenMinimizeButton(Window obj, bool value) => obj.SetValue(IsHiddenMinimizeButtonProperty, value); + //#endregion + + //#region HideMaximizeButton + + //private static readonly RoutedEventHandler HideMaximizeButtonWhenLoadedDelegate = (sender, args) => { + // if (sender is Window == false) + // return; + + // var w = (Window)sender; + // HideMaximizeButton(w); + // w.Loaded -= HideMaximizeButtonWhenLoadedDelegate; + //}; + + //private static readonly RoutedEventHandler ShowMaximizeButtonWhenLoadedDelegate = (sender, args) => { + // if (sender is Window == false) + // return; + + // var w = (Window)sender; + // ShowMaximizeButton(w); + // w.Loaded -= ShowMaximizeButtonWhenLoadedDelegate; + //}; + + //public static readonly DependencyProperty HideMaximizeButtonProperty = + // DependencyProperty.RegisterAttached("HideMaximizeButton", typeof(bool), OwnerType, + // new FrameworkPropertyMetadata(false, new PropertyChangedCallback(HideMaximizeButtonChangedCallback))); + + //[AttachedPropertyBrowsableForType(typeof(Window))] + //public static bool GetHideMaximizeButton(Window obj) => (bool)obj.GetValue(HideCloseButtonProperty); + + //[AttachedPropertyBrowsableForType(typeof(Window))] + //public static void SetHideMaximizeButton(Window obj, bool value) => obj.SetValue(HideCloseButtonProperty, value); + + //private static void HideMaximizeButtonChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) + //{ + // var window = d as Window; + // if (window == null) return; + + // var hideCloseButton = (bool)e.NewValue; + // if (hideCloseButton && !GetIsHiddenMaximizeButton(window)) + // { + // if (!window.IsLoaded) + // window.Loaded += HideMaximizeButtonWhenLoadedDelegate; + // else + // HideMaximizeButton(window); + + // SetIsHiddenMaximizeButton(window, true); + // } + // else if (!hideCloseButton && GetIsHiddenMaximizeButton(window)) + // { + // if (!window.IsLoaded) + // window.Loaded -= ShowMaximizeButtonWhenLoadedDelegate; + // else + // ShowMaximizeButton(window); + + // SetIsHiddenMaximizeButton(window, false); + // } + //} + + //private static void HideMaximizeButton(Window w) + //{ + // var hwnd = new WindowInteropHelper(w).Handle; + // SetWindowLong(hwnd, GWL_STYLE, GetWindowLong(hwnd, GWL_STYLE) & ~WS_MINIMIZEBOX); + //} + + //private static void ShowMaximizeButton(Window w) + //{ + // var hwnd = new WindowInteropHelper(w).Handle; + // SetWindowLong(hwnd, GWL_STYLE, GetWindowLong(hwnd, GWL_STYLE) | WS_MINIMIZEBOX); + //} + + //#endregion + + //#region IsHiddenMaximizeButton (readonly attached property) + //private static readonly DependencyPropertyKey IsHiddenMaximizeButtonKey = + // DependencyProperty.RegisterAttachedReadOnly("IsHiddenMaximizeButton", typeof(bool), OwnerType, new FrameworkPropertyMetadata(false)); + + //public static readonly DependencyProperty IsHiddenMaximizeButtonProperty = IsHiddenMaximizeButtonKey.DependencyProperty; + + //[AttachedPropertyBrowsableForType(typeof(Window))] + //public static bool GetIsHiddenMaximizeButton(Window obj) => (bool)obj.GetValue(IsHiddenMaximizeButtonProperty); + + //private static void SetIsHiddenMaximizeButton(Window obj, bool value) => obj.SetValue(IsHiddenMaximizeButtonProperty, value); + //#endregion + + + } +} diff --git a/src/WPFEduSolution/WPFBeginner/Configurations/AppSettings.cs b/src/WPFEduSolution/WPFBeginner/Configurations/AppSettings.cs new file mode 100644 index 0000000..45a9963 --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/Configurations/AppSettings.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using System.Windows; +using WPFBeginner.Models; + +namespace WPFBeginner.Configurations +{ + public class AppSettings + { + public EventHandler LanguageChanged; + public EventHandler ThemeChanged; + + [JsonPropertyName("Login")] + public LoginInfo Login { get; set;} + + private string language; + [JsonPropertyName("Language")] + public string Language + { + get => language; + set + { + var eventArg = new StringChangedEventArg() + { + Old = language, + New = value + }; + + language = value; + + if (eventArg.NotChanged) + return; + + LanguageChanged?.Invoke(null, eventArg); + } + } + + private string theme; + [JsonPropertyName("Theme")] + public string Theme + { + get => theme; + set + { + var eventArg = new StringChangedEventArg() + { + Old = language, + New = value + }; + + theme = value; + + if (eventArg.NotChanged) + return; + + ThemeChanged?.Invoke(null, eventArg); + } + } + + [JsonPropertyName("DefaultMembers")] + public List Members { get; set;} + } + + public class StringChangedEventArg : EventArgs + { + public string Old { get; set; } + public string New { get; set; } + + public bool NotChanged => Old?.Equals(New, StringComparison.OrdinalIgnoreCase) ?? true; + } +} diff --git a/src/WPFEduSolution/WPFBeginner/Configurations/LoginInfo.cs b/src/WPFEduSolution/WPFBeginner/Configurations/LoginInfo.cs new file mode 100644 index 0000000..2eeda77 --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/Configurations/LoginInfo.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace WPFBeginner.Configurations +{ + public class LoginInfo + { + [JsonPropertyName("User")] + public string User { get; set; } + + public string Password { get; set; } + } +} diff --git a/src/WPFEduSolution/WPFBeginner/Controls/MemberInputPanel.xaml b/src/WPFEduSolution/WPFBeginner/Controls/MemberInputPanel.xaml new file mode 100644 index 0000000..243b793 --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/Controls/MemberInputPanel.xaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/WPFEduSolution/WPFBeginner/Controls/MemberInputPanel.xaml.cs b/src/WPFEduSolution/WPFBeginner/Controls/MemberInputPanel.xaml.cs new file mode 100644 index 0000000..de0aebd --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/Controls/MemberInputPanel.xaml.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace WPFBeginner.Controls +{ + /// + /// MemberInputPanel.xaml에 대한 상호 작용 논리 + /// + public partial class MemberInputPanel : UserControl + { + public static readonly DependencyProperty MemberNameProperty = + DependencyProperty.Register(nameof(MemberName), typeof(string), typeof(MemberInputPanel), new FrameworkPropertyMetadata(){BindsTwoWayByDefault = true}); + + public string MemberName + { + get => (string)GetValue(MemberNameProperty); + set => SetValue(MemberNameProperty, value); + } + + public static readonly DependencyProperty EmployeeNoProperty = + DependencyProperty.Register(nameof(EmployeeNo), typeof(string), typeof(MemberInputPanel), new FrameworkPropertyMetadata() { BindsTwoWayByDefault = true }); + + public string EmployeeNo + { + get => (string)GetValue(EmployeeNoProperty); + set => SetValue(EmployeeNoProperty, value); + } + + public static readonly DependencyProperty CallProperty = + DependencyProperty.Register(nameof(Call), typeof(string), typeof(MemberInputPanel), new FrameworkPropertyMetadata() { BindsTwoWayByDefault = true }); + + public string Call + { + get => (string)GetValue(CallProperty); + set => SetValue(CallProperty, value); + } + + public static readonly DependencyProperty EmailProperty = + DependencyProperty.Register(nameof(Email), typeof(string), typeof(MemberInputPanel), new FrameworkPropertyMetadata() { BindsTwoWayByDefault = true }); + + public string Email + { + get => (string)GetValue(EmailProperty); + set => SetValue(EmailProperty, value); + } + + public MemberInputPanel() + { + InitializeComponent(); + } + } +} diff --git a/src/WPFEduSolution/WPFBeginner/Converters/BaseConverter.cs b/src/WPFEduSolution/WPFBeginner/Converters/BaseConverter.cs new file mode 100644 index 0000000..69cca88 --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/Converters/BaseConverter.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Markup; + +namespace WPFBeginner.Converters +{ + public abstract class BaseConverter : MarkupExtension + { + protected BaseConverter() { } + + public override object ProvideValue(IServiceProvider serviceProvider) + { + return this; + } + } +} diff --git a/src/WPFEduSolution/WPFBeginner/Converters/InverseBoolConverter.cs b/src/WPFEduSolution/WPFBeginner/Converters/InverseBoolConverter.cs new file mode 100644 index 0000000..c4e6edb --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/Converters/InverseBoolConverter.cs @@ -0,0 +1,20 @@ +using System.Windows.Data; + +namespace WPFBeginner.Converters +{ + [ValueConversion(typeof(bool), typeof(bool))] + public class InverseBoolConverter : BaseConverter, IValueConverter + { + public InverseBoolConverter() { } + + public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + return !(bool)value; + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + return !(bool)value; + } + } +} diff --git a/src/WPFEduSolution/WPFBeginner/Exceptions/AlreadyExcutedAppException.cs b/src/WPFEduSolution/WPFBeginner/Exceptions/AlreadyExcutedAppException.cs new file mode 100644 index 0000000..0e5aabf --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/Exceptions/AlreadyExcutedAppException.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WPFBeginner.Exceptions +{ + internal class AlreadyExcutedAppException : Exception + { + public AlreadyExcutedAppException() : base() + { + + } + } +} diff --git a/src/WPFEduSolution/WPFBeginner/Exceptions/LoginFailedException.cs b/src/WPFEduSolution/WPFBeginner/Exceptions/LoginFailedException.cs new file mode 100644 index 0000000..b98f160 --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/Exceptions/LoginFailedException.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WPFBeginner.Exceptions +{ + internal class LoginFailedException : Exception + { + public LoginFailedException() : base() + { + + } + } +} diff --git a/src/WPFEduSolution/WPFBeginner/Models/Member.cs b/src/WPFEduSolution/WPFBeginner/Models/Member.cs new file mode 100644 index 0000000..4813ed0 --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/Models/Member.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace WPFBeginner.Models +{ + public class Member + { + [JsonPropertyName("Name")] + public string Name { get; set; } + [JsonPropertyName("Call")] + public string Call { get; set; } + [JsonPropertyName("EMail")] + public string EMail { get; set; } + [JsonPropertyName("No")] + public string EmployeeNo { get; set; } + + public static Member Create(string name, string call, string email, string employeeNo) + { + return new Member() + { + Name = name, + Call = call, + EMail = email, + EmployeeNo = employeeNo }; + } + } +} diff --git a/src/WPFEduSolution/WPFBeginner/Resources/Images/DrawingImages.xaml b/src/WPFEduSolution/WPFBeginner/Resources/Images/DrawingImages.xaml new file mode 100644 index 0000000..e2188a4 --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/Resources/Images/DrawingImages.xaml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/WPFEduSolution/WPFBeginner/Resources/Images/bg_login.jpeg b/src/WPFEduSolution/WPFBeginner/Resources/Images/bg_login.jpeg new file mode 100644 index 0000000..f59b076 Binary files /dev/null and b/src/WPFEduSolution/WPFBeginner/Resources/Images/bg_login.jpeg differ diff --git a/src/WPFEduSolution/WPFBeginner/Resources/Images/btn_search_001.png b/src/WPFEduSolution/WPFBeginner/Resources/Images/btn_search_001.png new file mode 100644 index 0000000..dfcd6cb Binary files /dev/null and b/src/WPFEduSolution/WPFBeginner/Resources/Images/btn_search_001.png differ diff --git a/src/WPFEduSolution/WPFBeginner/Resources/Images/ico_pw.png b/src/WPFEduSolution/WPFBeginner/Resources/Images/ico_pw.png new file mode 100644 index 0000000..7cd31c8 Binary files /dev/null and b/src/WPFEduSolution/WPFBeginner/Resources/Images/ico_pw.png differ diff --git a/src/WPFEduSolution/WPFBeginner/Resources/Images/ico_user.png b/src/WPFEduSolution/WPFBeginner/Resources/Images/ico_user.png new file mode 100644 index 0000000..9d85395 Binary files /dev/null and b/src/WPFEduSolution/WPFBeginner/Resources/Images/ico_user.png differ diff --git a/src/WPFEduSolution/WPFBeginner/Resources/SharedResourceDictionary.cs b/src/WPFEduSolution/WPFBeginner/Resources/SharedResourceDictionary.cs new file mode 100644 index 0000000..078f976 --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/Resources/SharedResourceDictionary.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace WPFBeginner.Resources +{ + public class SharedResourceDictionary : ResourceDictionary + { + private static Dictionary _cache = new Dictionary(); + + private Uri source; + + public new Uri Source + { + get => source; + set + { + source = value; + if (!_cache.ContainsKey(source)) + { + AddToCache(); + } + else + { + WeakReference weakReference = _cache[source]; + if (weakReference != null && weakReference.IsAlive) + MergedDictionaries.Add((ResourceDictionary)weakReference.Target); + else + AddToCache(); + + } + + } + } + + private void AddToCache() + { + base.Source = source; + if (_cache.ContainsKey(source)) + _cache.Remove(source); + + _cache.Add(source, new WeakReference(this, false)); + } + } +} diff --git a/src/WPFEduSolution/WPFBeginner/Services/EnterFocusAssistanct.cs b/src/WPFEduSolution/WPFBeginner/Services/EnterFocusAssistanct.cs new file mode 100644 index 0000000..b6758b2 --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/Services/EnterFocusAssistanct.cs @@ -0,0 +1,52 @@ +using System.Windows.Input; +using System.Windows; + +namespace WPFBeginner.Services +{ + public class EnterKeyTraversal + { + public static readonly DependencyProperty IsEnabledProperty = + DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(EnterKeyTraversal), new UIPropertyMetadata(false, IsEnabledChanged)); + + public static bool GetIsEnabled(DependencyObject obj) => (bool)obj.GetValue(IsEnabledProperty); + + public static void SetIsEnabled(DependencyObject obj, bool value) => obj.SetValue(IsEnabledProperty, value); + + static void PreviewKeyDown(object sender, KeyEventArgs e) + { + var uiElement = e.OriginalSource as FrameworkElement; + + if (e.Key == Key.Enter) + { + e.Handled = true; + uiElement.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next)); + } + } + + private static void Unloaded(object sender, RoutedEventArgs e) + { + var uiElement = sender as FrameworkElement; + if (uiElement == null) + return; + + uiElement.Unloaded -= Unloaded; + uiElement.PreviewKeyDown -= PreviewKeyDown; + } + + static void IsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var ue = d as FrameworkElement; + if (ue == null) return; + + if ((bool)e.NewValue) + { + ue.Unloaded += Unloaded; + ue.PreviewKeyDown += PreviewKeyDown; + } + else + { + ue.PreviewKeyDown -= PreviewKeyDown; + } + } + } +} diff --git a/src/WPFEduSolution/WPFBeginner/Services/PasswordBoxAssistant.cs b/src/WPFEduSolution/WPFBeginner/Services/PasswordBoxAssistant.cs new file mode 100644 index 0000000..8e07227 --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/Services/PasswordBoxAssistant.cs @@ -0,0 +1,95 @@ +using System.Windows.Controls; +using System.Windows; + +namespace WPFBeginner.Services +{ + public static class PasswordBoxAssistant + { + #region BoundPassword Property + + public static readonly DependencyProperty BoundPassword = + DependencyProperty.RegisterAttached(nameof(BoundPassword), typeof(string), typeof(PasswordBoxAssistant), new PropertyMetadata(string.Empty, OnBoundPasswordChanged)); + + public static string GetBoundPassword(DependencyObject dp) => (string)dp.GetValue(BoundPassword); + + public static void SetBoundPassword(DependencyObject dp, string value) => dp.SetValue(BoundPassword, value); + + private static void OnBoundPasswordChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var box = d as PasswordBox; + + // only handle this event when the property is attached to a PasswordBox + // and when the BindPassword attached property has been set to true + if (d == null || !GetEnableBindingPassword(d)) + return; + + // avoid recursive updating by ignoring the box's changed event + box.PasswordChanged -= HandlePasswordChanged; + + string newPassword = (string)e.NewValue; + + if (!GetUpdatingPassword(box)) + box.Password = newPassword; + + box.PasswordChanged += HandlePasswordChanged; + } + + #endregion + + #region EnableBindingPassword + + public static readonly DependencyProperty EnableBindingPassword = + DependencyProperty.RegisterAttached(nameof(EnableBindingPassword), typeof(bool), typeof(PasswordBoxAssistant), new PropertyMetadata(false, OnEnableBindingPasswordChanged)); + + public static bool GetEnableBindingPassword(DependencyObject dp) => (bool)dp.GetValue(EnableBindingPassword); + + public static void SetEnableBindingPassword(DependencyObject dp, bool value) => dp.SetValue(EnableBindingPassword, value); + + private static void OnEnableBindingPasswordChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e) + { + // when the BindPassword attached property is set on a PasswordBox, + // start listening to its PasswordChanged event + + var box = dp as PasswordBox; + + if (box == null) + return; + + bool wasBound = (bool)(e.OldValue); + bool needToBind = (bool)(e.NewValue); + + if (wasBound) + box.PasswordChanged -= HandlePasswordChanged; + + if (needToBind) + box.PasswordChanged += HandlePasswordChanged; + } + + #endregion + + #region UpdatingPassword + #endregion + + private static readonly DependencyProperty UpdatingPassword = + DependencyProperty.RegisterAttached(nameof(UpdatingPassword), typeof(bool), typeof(PasswordBoxAssistant), new PropertyMetadata(false)); + + private static bool GetUpdatingPassword(DependencyObject dp) => (bool)dp.GetValue(UpdatingPassword); + + private static void SetUpdatingPassword(DependencyObject dp, bool value) => dp.SetValue(UpdatingPassword, value); + + private static void HandlePasswordChanged(object sender, RoutedEventArgs e) + { + var box = sender as PasswordBox; + + // set a flag to indicate that we're updating the password + SetUpdatingPassword(box, true); + // push the new password into the BoundPassword property + SetBoundPassword(box, box.Password); + SetUpdatingPassword(box, false); + } + + + + + } +} diff --git a/src/WPFEduSolution/WPFBeginner/Services/ResourceExplorer.cs b/src/WPFEduSolution/WPFBeginner/Services/ResourceExplorer.cs new file mode 100644 index 0000000..b16b68f --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/Services/ResourceExplorer.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media; + +namespace WPFBeginner.Services +{ + internal static class ResourceExplorer + { + public static string GetStringResource(string key) + { + var cultureCode = App.Config.Language; + var rd = App.Current.Resources.MergedDictionaries.FirstOrDefault(i => i.Source.AbsolutePath.EndsWith($"{cultureCode}.xaml")); + if (rd == null) + return null; + + return rd[key] as string; + } + + public static void ChangeXamlResource(string oldRes, string newRes) + { + var found = App.Current.Resources.MergedDictionaries.Select((item, i) => new { Item = item, Index = i}).FirstOrDefault(i => i.Item.Source.AbsoluteUri.Equals($"{oldRes}", StringComparison.OrdinalIgnoreCase)); + if (found == null) + return; + + var newResource = new ResourceDictionary() + { + Source = new Uri(newRes, UriKind.RelativeOrAbsolute) + }; + App.Current.Resources.MergedDictionaries.Remove(found.Item); + App.Current.Resources.MergedDictionaries.Add(newResource); + } + + public static ResourceDictionary GetLanguageResourceDic(string name) + { + var foundName = $"pack://application:,,,/Themes/Cultures/{name}.xaml"; + //var foundName = $"/WPFBeginner;component/Themes/Cultures/{name}.xaml"; + var found = App.Current.Resources.MergedDictionaries.Select((item, i) => new { Item = item, Index = i }).FirstOrDefault(i => i.Item.Source.AbsoluteUri.Equals(foundName, StringComparison.OrdinalIgnoreCase)); + if (found == null) + return new ResourceDictionary() { Source = new Uri(foundName, UriKind.RelativeOrAbsolute) }; + + return found.Item; + } + + public static ResourceDictionary GetThemeResourceDic(string name) + { + var foundName = $"pack://application:,,,/Themes/Colors/{name}.xaml"; + //var foundName = $"/WPFBeginner;component/Themes/Colors/{name}.xaml"; + var found = App.Current.Resources.MergedDictionaries.Select((item, i) => new { Item = item, Index = i }).FirstOrDefault(i => i.Item.Source.AbsoluteUri.Equals(foundName, StringComparison.OrdinalIgnoreCase)); + if (found == null) + return new ResourceDictionary(){ Source = new Uri(foundName, UriKind.RelativeOrAbsolute) }; + + return found.Item; + } + + } +} diff --git a/src/WPFEduSolution/WPFBeginner/Services/ViewModelLocator.cs b/src/WPFEduSolution/WPFBeginner/Services/ViewModelLocator.cs new file mode 100644 index 0000000..f28ad5d --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/Services/ViewModelLocator.cs @@ -0,0 +1,15 @@ +using CommunityToolkit.Mvvm.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using WPFBeginner.ViewModels; + +namespace WPFBeginner.Services +{ + internal class ViewModelLocator + { + public MainWindowViewModel MainWindowViewModel => Ioc.Default.GetService(); + } +} diff --git a/src/WPFEduSolution/WPFBeginner/Spec.txt b/src/WPFEduSolution/WPFBeginner/Spec.txt new file mode 100644 index 0000000..9d3e2be --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/Spec.txt @@ -0,0 +1,8 @@ + + +## 요구사항 + +1. 프로그램 중복실행 방지 +2. 로그인 화면 + - 파일로부터 데이터 읽어와 비교 +3. diff --git a/src/WPFEduSolution/WPFBeginner/Themes/Colors/DefaultTheme.xaml b/src/WPFEduSolution/WPFBeginner/Themes/Colors/DefaultTheme.xaml new file mode 100644 index 0000000..b3899a4 --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/Themes/Colors/DefaultTheme.xaml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/WPFEduSolution/WPFBeginner/Themes/Colors/LightTheme.xaml b/src/WPFEduSolution/WPFBeginner/Themes/Colors/LightTheme.xaml new file mode 100644 index 0000000..446ec6c --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/Themes/Colors/LightTheme.xaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/WPFEduSolution/WPFBeginner/Themes/Cultures/en-US.xaml b/src/WPFEduSolution/WPFBeginner/Themes/Cultures/en-US.xaml new file mode 100644 index 0000000..f27922c --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/Themes/Cultures/en-US.xaml @@ -0,0 +1,31 @@ + + + Duplicated execution. + Please check your ID and password again. + Login + Please enter your ID and password. + + Contact Management Program + Name: + Contact + Employee Number: + Email: + No search condition provided. Please enter a search condition. + + Add + Search + Delete + Reset + + Name + Contact + Employee Number + Email + + Register + Cancel + Name or employee number is required. + + \ No newline at end of file diff --git a/src/WPFEduSolution/WPFBeginner/Themes/Cultures/ko-KR.xaml b/src/WPFEduSolution/WPFBeginner/Themes/Cultures/ko-KR.xaml new file mode 100644 index 0000000..3b142e5 --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/Themes/Cultures/ko-KR.xaml @@ -0,0 +1,31 @@ + + + 중복된 실행입니다. + 아이디와 비번을 다시 한번 확인해주세요 + 로그인 + 아이디와 비번을 입력해주세요 + + 연락처 관리 프로그램 + 이름: + 연락처 + 사번: + 이메일: + 검색 조건이 없습니다. 검색 조건을 입력해주세요 + + 추가 + 검 색 + 삭제 + 초기화 + + 이름 + 연락처 + 사번 + 이메일 + + 등록 + 취소 + 이름 또는 사번은 반드시 필요합니다. + + \ No newline at end of file diff --git a/src/WPFEduSolution/WPFBeginner/Themes/Styles/Buttons.xaml b/src/WPFEduSolution/WPFBeginner/Themes/Styles/Buttons.xaml new file mode 100644 index 0000000..ba639e6 --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/Themes/Styles/Buttons.xaml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/WPFEduSolution/WPFBeginner/ViewModels/LoginWindowViewModel.cs b/src/WPFEduSolution/WPFBeginner/ViewModels/LoginWindowViewModel.cs new file mode 100644 index 0000000..bd3b7a4 --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/ViewModels/LoginWindowViewModel.cs @@ -0,0 +1,37 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using System.Windows; +using System.Windows.Input; +using WPFBeginner.Services; + +namespace WPFBeginner.ViewModels +{ + public partial class LoginWindowViewModel : ObservableObject + { + [ObservableProperty] + private string user; + + [ObservableProperty] + private string password; + + [ObservableProperty] + private string message; + + public ICommand LoginClickCommand => new RelayCommand((window) => + { + if (CheckID() && CheckPassword()) + window.DialogResult = true; + else + Message = ResourceExplorer.GetStringResource("Cultures.LoginWindow.ErrorMessage.InvalidUser"); + }); + + public ICommand CloseClickCommand => new RelayCommand((window) => + { + window.DialogResult = false; + }); + + private bool CheckID() => user?.Equals(App.Config.Login.User, StringComparison.OrdinalIgnoreCase) ?? false; + + private bool CheckPassword() => user?.Equals(App.Config.Login.User, StringComparison.OrdinalIgnoreCase) ?? false; + } +} diff --git a/src/WPFEduSolution/WPFBeginner/ViewModels/MainWindowViewModel.cs b/src/WPFEduSolution/WPFBeginner/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..e063474 --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,201 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using System.Collections.ObjectModel; +using System.Windows; +using System.Windows.Input; +using WPFBeginner.Models; +using WPFBeginner.Services; +using WPFBeginner.Views; + +namespace WPFBeginner.ViewModels +{ + public partial class MainWindowViewModel : ObservableRecipient + { + private ObservableCollection members; + public ObservableCollection Members + { + get + { + if(filterMemebers != null) + return filterMemebers; + + return members; + } + set => SetProperty(ref members, value); + } + + private ObservableCollection filterMemebers; + + [ObservableProperty] + private Member selectedMember; + + private Member searchModel; + + public string MemberName + { + get => searchModel?.Name; + set + { + if (searchModel == null) + searchModel = new Member(); + + searchModel.Name = value; + OnPropertyChanged(); + } + } + + public string Call + { + get => searchModel?.Call; + set + { + if (searchModel == null) + searchModel = new Member(); + + searchModel.Call = value; + OnPropertyChanged(); + } + } + public string EmployeeNo + { + get => searchModel?.EmployeeNo; + set + { + if (searchModel == null) + searchModel = new Member(); + + searchModel.EmployeeNo = value; + OnPropertyChanged(); + } + } + + public string EMail + { + get => searchModel.EMail; + set + { + if (searchModel == null) + searchModel = new Member(); + + searchModel.EMail = value; + OnPropertyChanged(); + } + } + + private bool isKorean; + public bool IsKorean + { + get => isKorean; + set + { + SetProperty(ref isKorean, value); + App.Config.Language = value ? "ko-KR" : "en-US"; + } + } + + private bool isDefaultTheme; + public bool IsDefaultTheme + { + get => isDefaultTheme; + set + { + SetProperty(ref isDefaultTheme, value); + App.Config.Theme = value ? "DefaultTheme" : "LightTheme"; + } + } + + public ICommand SearchClickCommand => new RelayCommand(() => + { + var errorMessage = ValidateSearchValues(); + if (errorMessage != null) + { + MessageBox.Show(errorMessage); + filterMemebers = null; + return; + } + + + IEnumerable result = Members; + + if (!string.IsNullOrWhiteSpace(MemberName)) + result = result.Where(i => i.Name?.Contains(MemberName) ?? false); + + if (!string.IsNullOrWhiteSpace(EMail)) + result = result.Where(i => i.EMail?.Contains(EMail) ?? false); + + if (!string.IsNullOrWhiteSpace(Call)) + result = result.Where(i => i.Call?.Contains(Call) ?? false); + + if (!string.IsNullOrWhiteSpace(EmployeeNo)) + result = result.Where(i => i.EmployeeNo?.Contains(EmployeeNo) ?? false); + + if (result == null) + filterMemebers = new ObservableCollection(); + else + filterMemebers = new ObservableCollection(result); + + OnPropertyChanged(nameof(Members)); + }); + + + private string ValidateSearchValues() + { + if (string.IsNullOrWhiteSpace(MemberName) + && string.IsNullOrWhiteSpace(EmployeeNo) + && string.IsNullOrWhiteSpace(Call) + && string.IsNullOrWhiteSpace(EMail)) + return ResourceExplorer.GetStringResource("Cultures.MainWindow.ErrorMessage.SearchConditionEmpty"); + + return null; + } + + public ICommand DeleteClickCommand => new RelayCommand(() => + { + if (SelectedMember == null) + return; + + Members.Remove(SelectedMember); + }); + + public ICommand InitClickCommand => new RelayCommand(() => + { + filterMemebers = null; + searchModel = new Member(); + OnPropertyChanged(nameof(MemberName)); + OnPropertyChanged(nameof(EmployeeNo)); + OnPropertyChanged(nameof(EMail)); + OnPropertyChanged(nameof(Call)); + OnPropertyChanged(nameof(Members)); + }); + + public ICommand AddClickCommand => new RelayCommand(() => + { + var registDialog = new RegistMemberWindow(); + var result = registDialog.ShowDialog(); + if (result == true) + { + var dx = registDialog.DataContext as RegistMemberWindowViewModel; + if (dx == null) + return; + Members.Add(Member.Create(dx.MemberName, dx.Call, dx.Email, dx.EmployeeNo)); + } + }); + + + public MainWindowViewModel() + { + CreateDefaultMember(); + searchModel = new Member(); + isKorean = App.Config.Language.Equals("ko-KR", StringComparison.OrdinalIgnoreCase); + isDefaultTheme = App.Config.Theme.Equals("DefaultTheme", StringComparison.OrdinalIgnoreCase); + } + + private void CreateDefaultMember() + { + members = new ObservableCollection(); + foreach (var member in App.Config.Members) + members.Add(Member.Create(member.Name, member.Call, member.EMail, member.EmployeeNo)); + OnPropertyChanged(nameof(Members)); + } + } +} diff --git a/src/WPFEduSolution/WPFBeginner/ViewModels/RegistMemberWindowViewModel.cs b/src/WPFEduSolution/WPFBeginner/ViewModels/RegistMemberWindowViewModel.cs new file mode 100644 index 0000000..5c06a84 --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/ViewModels/RegistMemberWindowViewModel.cs @@ -0,0 +1,48 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using System.ComponentModel; +using System.Diagnostics; +using System.Windows; +using System.Windows.Input; +using WPFBeginner.Services; +using WPFBeginner.Views; + +namespace WPFBeginner.ViewModels +{ + public partial class RegistMemberWindowViewModel : ObservableRecipient + { + [ObservableProperty] + private string memberName; + + [ObservableProperty] + private string call; + + [ObservableProperty] + private string employeeNo; + + [ObservableProperty] + private string email; + + public ICommand RegistClickCommand => new RelayCommand((window) => + { + var result = Validate(); + if (result != null) + MessageBox.Show(result); + else + window.DialogResult = true; + }); + + public ICommand CancelClickCommand => new RelayCommand((window) => + { + window.DialogResult = false; + }); + + private string Validate() + { + if (string.IsNullOrWhiteSpace(MemberName) || string.IsNullOrWhiteSpace(EmployeeNo)) + return ResourceExplorer.GetStringResource("Cultures.RegistMemberWindow.ErrorMessage.FillMustValue"); + + return null; + } + } +} diff --git a/src/WPFEduSolution/WPFBeginner/Views/LoginWindow.xaml b/src/WPFEduSolution/WPFBeginner/Views/LoginWindow.xaml new file mode 100644 index 0000000..bea2b64 --- /dev/null +++ b/src/WPFEduSolution/WPFBeginner/Views/LoginWindow.xaml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +