├── .gitattributes
├── .gitignore
├── HISTORY.md
├── Images
├── Privacy_location_en.png
├── Privacy_location_jp.png
├── Screenshot_main.png
└── Screenshot_organize.png
├── LICENSE.txt
├── PRIVACY.md
├── README.md
├── README_ja.md
└── Source
├── .editorconfig
├── Directory.Build.props
├── IconImage
├── App.config
├── App.xaml
├── App.xaml.cs
├── DarkAppIcon.xaml
├── DarkAppIcon.xaml.cs
├── FrameworkElementImage.cs
├── IconImage.csproj
├── LightAppIcon.xaml
├── LightAppIcon.xaml.cs
├── MainWindow.xaml
├── MainWindow.xaml.cs
├── Properties
│ ├── AssemblyInfo.cs
│ ├── Resources.Designer.cs
│ ├── Resources.resx
│ ├── Settings.Designer.cs
│ └── Settings.settings
├── ScaleConverter.cs
├── UpdateImage.xaml
└── UpdateImage.xaml.cs
├── Installer
├── Installer.wixproj
├── Product.wxs
└── Resources
│ ├── banner.png
│ └── dialog.png
├── ReactivePropertyTest
├── App.config
├── App.xaml
├── App.xaml.cs
├── BindableBase.cs
├── MainWindow.xaml
├── MainWindow.xaml.cs
├── MainWindowViewModel.cs
├── MemberViewModel.cs
├── ObservableExtension.cs
├── Properties
│ ├── AssemblyInfo.cs
│ ├── Resources.Designer.cs
│ ├── Resources.resx
│ ├── Settings.Designer.cs
│ └── Settings.settings
└── ReactivePropertyTest.csproj
├── VisualStateTest
├── App.config
├── App.xaml
├── App.xaml.cs
├── MainWindow.xaml
├── MainWindow.xaml.cs
├── Properties
│ ├── AssemblyInfo.cs
│ ├── Resources.Designer.cs
│ ├── Resources.resx
│ ├── Settings.Designer.cs
│ └── Settings.settings
├── VisualStateBehavior.cs
├── VisualStateMonitor.cs
└── VisualStateTest.csproj
├── Wifinian.Test
├── LanguageServiceTest.cs
├── Properties
│ └── AssemblyInfo.cs
└── Wifinian.Test.csproj
├── Wifinian.sln
└── Wifinian
├── App.config
├── App.xaml
├── App.xaml.cs
├── AppController.cs
├── Appkeeper.cs
├── Common
├── BindableBase.cs
└── DisposableBase.cs
├── Helper
├── ColorExtension.cs
├── HsbColor.cs
└── OsVersion.cs
├── Library
├── ScreenFrame.dll
├── ScreenFrame.xml
├── StartupAgency.dll
└── StartupAgency.xml
├── Models
├── AppDataService.cs
├── LanguageService.cs
├── LocationInfo.cs
├── Logger.cs
├── ProductInfo.cs
├── Settings.cs
└── Wlan
│ ├── IWlanWorker.cs
│ ├── MockWorker.cs
│ ├── NativeWifiProfileItem.cs
│ ├── NativeWifiWorker.cs
│ ├── Netsh.cs
│ ├── NetshWorker.cs
│ └── ProfileItem.cs
├── Properties
├── AssemblyInfo.cs
├── Resources.Designer.cs
└── Resources.resx
├── Resources
├── Icons
│ ├── AppIcon.ico
│ ├── DarkTrayIcon.ico
│ └── LightTrayIcon.ico
├── language.en.txt
└── language.ja-JP.txt
├── ViewModels
├── MainWindowViewModel.cs
├── MenuWindowViewModel.cs
└── ProfileItemViewModel.cs
├── Views
├── Behaviors
│ ├── FrameworkElementCenterBehavior.cs
│ ├── ListBoxHeightBehavior.cs
│ ├── ListBoxSelectedItemBehavior.cs
│ └── WindowMaximizeBoxBehavior.cs
├── Controls
│ ├── IconButton.cs
│ ├── MovingButton.cs
│ ├── MovingControl.cs
│ ├── ProfileContentControl.cs
│ ├── SignalProgressBar.cs
│ ├── SimpleToggleButton.cs
│ └── SwitchTextBox.cs
├── Converters
│ ├── BooleanInverseConverter.cs
│ └── VisibilityInverseConverter.cs
├── MainWindow.xaml
├── MainWindow.xaml.cs
├── MenuWindow.xaml
├── MenuWindow.xaml.cs
├── Styles
│ ├── CommonControls.xaml
│ ├── CustomControls.xaml
│ └── CustomTheme.xaml
├── ThemeService.cs
└── WindowPainter.cs
├── Wifinian.VisualElementsManifest.xml
├── Wifinian.csproj
└── app.manifest
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | #* text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | x64/
19 | x86/
20 | build/
21 | bld/
22 | [Bb]in/
23 | [Oo]bj/
24 |
25 | # Visual Studio 2015 cache/options directory
26 | .vs/
27 |
28 | # MSTest test Results
29 | [Tt]est[Rr]esult*/
30 | [Bb]uild[Ll]og.*
31 |
32 | # NUNIT
33 | *.VisualState.xml
34 | TestResult.xml
35 |
36 | # Build Results of an ATL Project
37 | [Dd]ebugPS/
38 | [Rr]eleasePS/
39 | dlldata.c
40 |
41 | # DNX
42 | project.lock.json
43 | artifacts/
44 |
45 | *_i.c
46 | *_p.c
47 | *_i.h
48 | *.ilk
49 | *.meta
50 | *.obj
51 | *.pch
52 | *.pdb
53 | *.pgc
54 | *.pgd
55 | *.rsp
56 | *.sbr
57 | *.tlb
58 | *.tli
59 | *.tlh
60 | *.tmp
61 | *.tmp_proj
62 | *.log
63 | *.vspscc
64 | *.vssscc
65 | .builds
66 | *.pidb
67 | *.svclog
68 | *.scc
69 |
70 | # Chutzpah Test files
71 | _Chutzpah*
72 |
73 | # Visual C++ cache files
74 | ipch/
75 | *.aps
76 | *.ncb
77 | *.opensdf
78 | *.sdf
79 | *.cachefile
80 |
81 | # Visual Studio profiler
82 | *.psess
83 | *.vsp
84 | *.vspx
85 |
86 | # TFS 2012 Local Workspace
87 | $tf/
88 |
89 | # Guidance Automation Toolkit
90 | *.gpState
91 |
92 | # ReSharper is a .NET coding add-in
93 | _ReSharper*/
94 | *.[Rr]e[Ss]harper
95 | *.DotSettings.user
96 |
97 | # JustCode is a .NET coding add-in
98 | .JustCode
99 |
100 | # TeamCity is a build add-in
101 | _TeamCity*
102 |
103 | # DotCover is a Code Coverage Tool
104 | *.dotCover
105 |
106 | # NCrunch
107 | _NCrunch_*
108 | .*crunch*.local.xml
109 |
110 | # MightyMoose
111 | *.mm.*
112 | AutoTest.Net/
113 |
114 | # Web workbench (sass)
115 | .sass-cache/
116 |
117 | # Installshield output folder
118 | [Ee]xpress/
119 |
120 | # DocProject is a documentation generator add-in
121 | DocProject/buildhelp/
122 | DocProject/Help/*.HxT
123 | DocProject/Help/*.HxC
124 | DocProject/Help/*.hhc
125 | DocProject/Help/*.hhk
126 | DocProject/Help/*.hhp
127 | DocProject/Help/Html2
128 | DocProject/Help/html
129 |
130 | # Click-Once directory
131 | publish/
132 |
133 | # Publish Web Output
134 | *.[Pp]ublish.xml
135 | *.azurePubxml
136 | ## TODO: Comment the next line if you want to checkin your
137 | ## web deploy settings but do note that will include unencrypted
138 | ## passwords
139 | #*.pubxml
140 |
141 | *.publishproj
142 |
143 | # NuGet Packages
144 | *.nupkg
145 | # The packages folder can be ignored because of Package Restore
146 | **/packages/*
147 | # except build/, which is used as an MSBuild target.
148 | !**/packages/build/
149 | # Uncomment if necessary however generally it will be regenerated when needed
150 | #!**/packages/repositories.config
151 |
152 | # Windows Azure Build Output
153 | csx/
154 | *.build.csdef
155 |
156 | # Windows Store app package directory
157 | AppPackages/
158 |
159 | # Visual Studio cache files
160 | # files ending in .cache can be ignored
161 | *.[Cc]ache
162 | # but keep track of directories ending in .cache
163 | !*.[Cc]ache/
164 |
165 | # Others
166 | ClientBin/
167 | [Ss]tyle[Cc]op.*
168 | ~$*
169 | *~
170 | *.dbmdl
171 | *.dbproj.schemaview
172 | *.pfx
173 | *.publishsettings
174 | node_modules/
175 | orleans.codegen.cs
176 |
177 | # RIA/Silverlight projects
178 | Generated_Code/
179 |
180 | # Backup & report files from converting an old project file
181 | # to a newer Visual Studio version. Backup files are not needed,
182 | # because we have git ;-)
183 | _UpgradeReport_Files/
184 | Backup*/
185 | UpgradeLog*.XML
186 | UpgradeLog*.htm
187 |
188 | # SQL Server files
189 | *.mdf
190 | *.ldf
191 |
192 | # Business Intelligence projects
193 | *.rdl.data
194 | *.bim.layout
195 | *.bim_*.settings
196 |
197 | # Microsoft Fakes
198 | FakesAssemblies/
199 |
200 | # Node.js Tools for Visual Studio
201 | .ntvs_analysis.dat
202 |
203 | # Visual Studio 6 build log
204 | *.plg
205 |
206 | # Visual Studio 6 workspace options file
207 | *.opt
208 |
209 | # LightSwitch generated files
210 | GeneratedArtifacts/
211 | _Pvt_Extensions/
212 | ModelManifest.xml
--------------------------------------------------------------------------------
/HISTORY.md:
--------------------------------------------------------------------------------
1 | ## History
2 |
3 | Ver 3.7 2025-1-25
4 |
5 | - Update WPA3 authentications
6 |
7 | Ver 3.6 2025-1-14
8 |
9 | - Add notification for Windows 11 24H2
10 |
11 | Ver 3.5 2024-7-6
12 |
13 | - Add support of 6GHz band
14 |
15 | Ver 3.4 2024-1-14
16 |
17 | - Modify icons
18 |
19 | Ver 3.3 2022-8-2
20 |
21 | - Modify to show Wi-Fi protocol
22 |
23 | Ver 3.2 2021-12-9
24 |
25 | - Modify Engage function
26 |
27 | Ver 3.1 2021-12-3
28 |
29 | - Update libraries
30 |
31 | Ver 3.0 2021-9-9
32 |
33 | - Change UI
34 | - Make rounded corners default on Windows 11
35 |
36 | Ver 2.9 2021-4-12
37 |
38 | - Add function to show available networks only
39 |
40 | Ver 2.8 2021-3-30
41 |
42 | - Modify handling of invalid information
43 |
44 | Ver 2.7 2021-3-28
45 |
46 | - Update libraries
47 |
48 | Ver 2.6 2021-1-25
49 |
50 | - Improve codes
51 |
52 | Ver 2.4 2020-9-29
53 |
54 | - Adapt WPA3
55 | - Change to save Engage state
56 |
57 | Ver 2.3 2019-12-6
58 |
59 | - Change function name to Organize
60 |
61 | Ver 2.2 2018-9-2
62 |
63 | - Add function to show frequency band and channel of wireless LAN
64 |
65 | Ver 2.1 2018-2-19
66 |
67 | - Added handling when wireless functions are not available
68 | - Extended startup function
69 |
70 | Ver 2.0 2018-1-4
71 |
72 | - Recreated and renamed
73 |
74 | Ver 1.1 2015-12-2
75 |
76 | - Fixed exception when loading settings file
77 | - Changed target framework to 4.6
78 | - Added flyout to notification area icon
79 |
80 | Ver 1.1 2015-11-13
81 |
82 | - Switched from internal NativeWifi to ManagedNativeWifi
83 |
84 | Ver 1.0 2015-8-22
85 |
86 | - Initial release
--------------------------------------------------------------------------------
/Images/Privacy_location_en.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emoacht/Wifinian/e17e5989de2be66b3731dcf2a7dc88a505bd5e3f/Images/Privacy_location_en.png
--------------------------------------------------------------------------------
/Images/Privacy_location_jp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emoacht/Wifinian/e17e5989de2be66b3731dcf2a7dc88a505bd5e3f/Images/Privacy_location_jp.png
--------------------------------------------------------------------------------
/Images/Screenshot_main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emoacht/Wifinian/e17e5989de2be66b3731dcf2a7dc88a505bd5e3f/Images/Screenshot_main.png
--------------------------------------------------------------------------------
/Images/Screenshot_organize.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emoacht/Wifinian/e17e5989de2be66b3731dcf2a7dc88a505bd5e3f/Images/Screenshot_organize.png
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015-2019 emoacht
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/PRIVACY.md:
--------------------------------------------------------------------------------
1 | ## Privacy Policy
2 |
3 | This application, Wifinian, does not collect personal information.
4 |
5 | Note: This notice is required by Microsoft Store because this application declares full trust capability, regardless of its actual functions. That capability is the requirement of Desktop Bridge for being distributed through the Store.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | English|[Japanese](README_ja.md)
2 | -|-
3 |
4 | # Wifinian
5 |
6 | More control over Wi-Fi connections!
7 |
8 | Wifinian is a Windows desktop tool to enable user to actively control Wi-Fi connections.
9 |
10 | 
11 | (DPI: 200%)
12 |
13 | Functions:
14 | * Connect to or disconnect from a wireless network
15 | * Rename a wireless profile
16 | * Change automatic connection (Auto Connect) or automatic switch (Auto Switch) settings of a wireless profile
17 | * __Rush__ - Perform rescan of wireless networks in short intervals (The number indicates interval (sec).)
18 | * __Engage__ - Execute automatic connection depending on signal strength, order and automatic switch setting of wireless profiles (The number indicates threshold of signal strength (%).)
19 | * __Organize__ - Change the order (priority) of wireless profiles, delete a wireless profile
20 |
21 | 
22 | (DPI: 100%)
23 |
24 | ## Requirements
25 |
26 | * Windows 7 or newer
27 | * .NET Framework 4.8
28 | * On Windows 11 (24H2) or newer, in Privacy & security > Location settings, access to location information needs to be turned on.
29 |
30 |
31 | ## Download
32 |
33 | * Microsoft Store (Windows 10 (1607) or newer):
34 | [Wifinian](https://www.microsoft.com/store/apps/9pngfqps4flh)
35 |
36 |
37 | * Winget (a.k.a. [Windows Package Manager](https://docs.microsoft.com/en-us/windows/package-manager), App Installer):
38 | ```
39 | winget install Wifinian
40 | ```
41 |
42 | * Other:
43 | :floppy_disk: [Installer](https://github.com/emoacht/Wifinian/releases/download/3.7.1-Installer/WifinianInstaller371.zip)
44 |
45 | ## Install/Uninstall
46 |
47 | If you wish to place executable files on your own, you can extract them from installer file (.msi) by the following command:
48 |
49 | ```
50 | msiexec /a [source msi file path] targetdir=[destination folder path (absolute path)] /qn
51 | ```
52 |
53 | In such case, please note the following:
54 |
55 | - The settings file will be created at: `[system drive]\Users\[user name]\AppData\Local\Wifinian\`
56 | - When you check [Start on sign in], a registry value will be added to: `HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run`
57 |
58 | ## Remarks
59 |
60 | - To rename a wireless profile, press and hold its name.
61 | - Rescan of wireless networks by OS itself is triggered by some actions and if no such actions take place, seems to be performed once per one minute.
62 | - Automatic connection by Engage function applies only to wireless profiles whose automatic switch are enabled. If a profile whose automatic switch is not enabled has been already connected, automatic connection will not be executed.
63 | - When you connect to a wireless network by OS's wireless menu, the order of wireless profiles may be automatically changed.
64 | - A wireless profile is associated with a specific wireless adapter and if the adapter is not in place, such profile will not be shown.
65 |
66 | ## History
67 |
68 | :scroll: [History](HISTORY.md)
69 |
70 | ## Libraries
71 |
72 | - [Reactive Extensions][1]
73 | - [Reactive Property][2]
74 | - [XamlBehaviors for WPF][3]
75 | - [Managed Native Wifi][4]
76 | - [Monitorian/ScreenFrame][5]
77 | - [Monitorian/StartupAgency][5]
78 |
79 | [1]: https://github.com/Reactive-Extensions/Rx.NET
80 | [2]: https://github.com/runceel/ReactiveProperty
81 | [3]: https://github.com/microsoft/XamlBehaviorsWpf
82 | [4]: https://github.com/emoacht/ManagedNativeWifi
83 | [5]: https://github.com/emoacht/Monitorian
84 |
85 | ## License
86 |
87 | - MIT License
88 |
89 | ## Developer
90 |
91 | - emoacht (emotom[atmark]pobox.com)
92 |
93 | _____
94 |
95 | ### How to delete wireless profiles from OS's GUI
96 |
97 | The GUI to delete wireless profiles has come back in Windows 8.1 Update. To reach this GUI, see the following.
98 |
99 | #### Windows 10
100 |
101 | [Network settings] from notification area (or [Settings] from Start menu) → [Network & Internet] → [Wi-Fi] → [Manage WiFi Settings] → [Manage known networks]
102 |
103 | Note: If multiple profiles of the same name exist (it will happen if you connected to the same wireless network using multiple wireless adapters because SSID of wireless network is used for a profile name), such profiles will not be differentiated and will be deleted in bulk.
104 |
105 | #### Windows 8.1 Update
106 |
107 | [Settings] in Charm → [Change PC settings] → [Network] → [Connections] → [Manage known networks] in [Wi-Fi]
108 |
--------------------------------------------------------------------------------
/README_ja.md:
--------------------------------------------------------------------------------
1 | [English](README.md)|Japanese
2 | -|-
3 |
4 | # Wifinian
5 |
6 | Wi-Fi接続にもっとコントロールを!
7 |
8 | WifinianはWi-Fi接続をユーザーが積極的にコントロールできるWindowsデスクトップツールです。
9 |
10 | 
11 | (DPI: 200%)
12 |
13 | 機能:
14 | * 無線ネットワークとの接続と切断
15 | * 無線プロファイルの名称の変更
16 | * 無線プロファイルの自動接続(Auto Connect)と自動切換(Auto Switch)設定の変更
17 | * __Rush__ - 短い間隔での無線ネットワークの再スキャンの実行(数字は間隔(秒)を示す。)
18 | * __Engage__ - 無線プロファイルの電波強度、順番、自動切換の設定に応じた自動接続の実行(数字は電波強度(%)の閾値を示す。)
19 | * __Organize__ - 無線プロファイルの順番(優先度)の変更、無線プロファイルの削除
20 |
21 | 
22 | (DPI: 100%)
23 |
24 | ## 動作条件
25 |
26 | * Windows 7以降
27 | * .NET Framework 4.8
28 | * Windows 11 (24H2) 以降では、プライバシーとセキュリティ > 位置情報の設定で、位置情報へのアクセスがオンになっていることが必要。
29 |
30 |
31 | ## ダウンロード
32 |
33 | * Microsoft ストア (Windows 10 (1607) 以降):
34 | [Wifinian](https://www.microsoft.com/store/apps/9pngfqps4flh)
35 |
36 |
37 | * Winget (a.k.a. [Windows Package Manager](https://docs.microsoft.com/en-us/windows/package-manager), アプリインストーラー):
38 | ```
39 | winget install Wifinian
40 | ```
41 |
42 | * その他:
43 | :floppy_disk: [インストーラー](https://github.com/emoacht/Wifinian/releases/download/3.7.1-Installer/WifinianInstaller371.zip)
44 |
45 | ## インストール/アンインストール
46 |
47 | 実行ファイルを自分で配置したい場合には、インストーラーのファイル(.msi)から実行ファイルを次のコマンドで抽出できます。
48 |
49 | ```
50 | msiexec /a [source msi file path] targetdir=[destination folder path (absolute path)] /qn
51 | ```
52 |
53 | この場合、以下に留意してください。
54 |
55 | - 設定ファイルは次の場所に作成されます: `[system drive]\Users\[user name]\AppData\Local\Wifinian\`
56 | - [サインイン時に起動する]にチェックしたときは、レジストリ値が次の位置に追加されます: `HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run`
57 |
58 | ## 特記事項
59 |
60 | - 無線プロファイルの名称を変更するには、その名称を長押ししてください。
61 | - OS自身による無線ネットワークの再スキャンは、幾つかのアクションに応じて行われるほか、そのようなアクションがなければ、1分ごとに行われるようです。
62 | - Engage機能の自動接続は、自動切換が有効な無線プロファイルだけが対象です。自動切換が有効でない無線プロファイルが既に接続済みのときは、自動接続は行われません。
63 | - OSの無線メニューから接続すると、無線プロファイルの順番が自動的に変更されることがあります。
64 | - 無線プロファイルは特定の無線アダプターに関連付けられているので、そのアダプターが取り外されているときは表示されません。
65 |
66 | ## 履歴
67 |
68 | :scroll: [History](HISTORY.md)
69 |
70 | ## ライブラリ
71 |
72 | - [Reactive Extensions][1]
73 | - [Reactive Property][2]
74 | - [XamlBehaviors for WPF][3]
75 | - [Managed Native Wifi][4]
76 | - [Monitorian/ScreenFrame][5]
77 | - [Monitorian/StartupAgency][5]
78 |
79 | [1]: https://github.com/Reactive-Extensions/Rx.NET
80 | [2]: https://github.com/runceel/ReactiveProperty
81 | [3]: https://github.com/microsoft/XamlBehaviorsWpf
82 | [4]: https://github.com/emoacht/ManagedNativeWifi
83 | [5]: https://github.com/emoacht/Monitorian
84 |
85 | ## ライセンス
86 |
87 | - MIT License
88 |
89 | ## 開発者
90 |
91 | - emoacht (emotom[atmark]pobox.com)
92 |
93 | _____
94 |
95 | ### 無線プロファイルをOSのGUIから削除する方法
96 |
97 | 無線プロファイルを削除するためのGUIがWindows 8.1 Updateから復活しました。このGUIに辿り着くには以下を見てください。
98 |
99 | #### Windows 10
100 |
101 | 通知領域から[ネットワーク設定](またはスタートメニューから[設定])→ [ネットワークとインターネット] → [Wi-Fi] → [Wi-Fi設定を管理する] → [既知のネットワークの管理]
102 |
103 | 注意: 同名のプロファイルが複数ある場合(プロファイル名には無線ネットワークのSSIDが使われるので、同じ無線ネットワークに複数の無線アダプターで接続した場合に起こる)、これらは区別されず、まとめて削除されます。
104 |
105 | #### Windows 8.1 Update
106 |
107 | チャームの[設定] → [PC設定の変更] → [ネットワーク] → [接続] → [Wi-Fi]の[既知のネットワークの管理]
108 |
--------------------------------------------------------------------------------
/Source/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*.{cs,vb,xaml}]
4 | indent_style = tab
5 | indent_size = 4
6 | end_of_line = crlf
7 | insert_final_newline = false
8 | trim_trailing_whitespace = true
9 | charset = utf-8-bom
10 |
--------------------------------------------------------------------------------
/Source/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | latest
4 |
5 |
--------------------------------------------------------------------------------
/Source/IconImage/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/Source/IconImage/App.xaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Source/IconImage/App.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 |
3 | namespace IconImage;
4 |
5 | public partial class App : Application
6 | {
7 | }
--------------------------------------------------------------------------------
/Source/IconImage/DarkAppIcon.xaml:
--------------------------------------------------------------------------------
1 |
11 |
12 | #FF343434
13 | #FF262626
14 |
15 |
16 |
17 |
18 |
19 | 20
20 |
21 |
22 |
23 |
26 |
27 |
30 |
31 |
32 |
33 |
34 |
35 |
38 |
39 |
40 |
41 |
42 |
49 |
53 |
54 |
58 |
59 |
60 |
61 |
65 |
66 |
116 |
117 |
--------------------------------------------------------------------------------
/Source/IconImage/DarkAppIcon.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Windows.Controls;
2 |
3 | namespace IconImage;
4 |
5 | public partial class DarkAppIcon : UserControl
6 | {
7 | public DarkAppIcon()
8 | {
9 | InitializeComponent();
10 | }
11 | }
--------------------------------------------------------------------------------
/Source/IconImage/FrameworkElementImage.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Windows;
4 | using System.Windows.Media;
5 | using System.Windows.Media.Imaging;
6 |
7 | namespace IconImage;
8 |
9 | public class FrameworkElementImage
10 | {
11 | ///
12 | /// Saves the image of a specified FrameworkElement to file in PNG format.
13 | ///
14 | /// FrameworkElement
15 | /// File path
16 | /// Width of saved image (optional)
17 | /// Height of saved image (optional)
18 | public static void SaveImage(FrameworkElement source, string filePath, double width = 0D, double height = 0D)
19 | {
20 | if (source is null)
21 | throw new ArgumentNullException(nameof(source));
22 | if (string.IsNullOrWhiteSpace(filePath))
23 | throw new ArgumentNullException(nameof(filePath));
24 |
25 | var rtb = new RenderTargetBitmap(
26 | (int)source.Width,
27 | (int)source.Height,
28 | 96D, 96D,
29 | PixelFormats.Pbgra32);
30 |
31 | rtb.Render(source);
32 |
33 | var encoder = new PngBitmapEncoder();
34 | encoder.Frames.Add(BitmapFrame.Create(rtb));
35 |
36 | if ((0 < width) || (0 < height))
37 | {
38 | var bi = new BitmapImage();
39 |
40 | using (var ms = new MemoryStream())
41 | {
42 | encoder.Save(ms);
43 | ms.Seek(0, SeekOrigin.Begin);
44 |
45 | bi.BeginInit();
46 | bi.CacheOption = BitmapCacheOption.OnLoad;
47 | bi.StreamSource = ms;
48 |
49 | if (0 < width)
50 | bi.DecodePixelWidth = (int)width;
51 | if (0 < height)
52 | bi.DecodePixelHeight = (int)height;
53 |
54 | bi.EndInit();
55 | }
56 |
57 | encoder = new PngBitmapEncoder(); // Save method cannot be used twice.
58 | encoder.Frames.Add(BitmapFrame.Create(bi));
59 | }
60 |
61 | using (var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite))
62 | {
63 | encoder.Save(fs);
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/Source/IconImage/IconImage.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {566D20ED-A863-43BA-BC28-D21E46FEC661}
8 | WinExe
9 | Properties
10 | IconImage
11 | IconImage
12 | v4.8
13 | 512
14 | {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
15 | 4
16 | true
17 |
18 |
19 |
20 | AnyCPU
21 | true
22 | full
23 | false
24 | bin\Debug\
25 | DEBUG;TRACE
26 | prompt
27 | 4
28 |
29 |
30 | AnyCPU
31 | pdbonly
32 | true
33 | bin\Release\
34 | TRACE
35 | prompt
36 | 4
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | 4.0
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | MSBuild:Compile
55 | Designer
56 |
57 |
58 | DarkAppIcon.xaml
59 |
60 |
61 |
62 | LightAppIcon.xaml
63 |
64 |
65 |
66 | UpdateImage.xaml
67 |
68 |
69 | MSBuild:Compile
70 | Designer
71 |
72 |
73 | MSBuild:Compile
74 | Designer
75 |
76 |
77 | MSBuild:Compile
78 | Designer
79 |
80 |
81 | App.xaml
82 | Code
83 |
84 |
85 | MainWindow.xaml
86 | Code
87 |
88 |
89 | Designer
90 | MSBuild:Compile
91 |
92 |
93 |
94 |
95 | Code
96 |
97 |
98 | True
99 | True
100 | Resources.resx
101 |
102 |
103 | True
104 | Settings.settings
105 | True
106 |
107 |
108 | ResXFileCodeGenerator
109 | Resources.Designer.cs
110 |
111 |
112 | SettingsSingleFileGenerator
113 | Settings.Designer.cs
114 |
115 |
116 |
117 |
118 |
119 | Designer
120 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/Source/IconImage/LightAppIcon.xaml:
--------------------------------------------------------------------------------
1 |
11 |
12 | #FFEAEAEA
13 | White
14 |
15 |
16 |
17 |
18 |
19 | 20
20 |
21 |
22 |
23 |
26 |
27 |
30 |
31 |
32 |
33 |
34 |
35 |
38 |
39 |
40 |
41 |
42 |
49 |
53 |
54 |
58 |
59 |
60 |
61 |
107 |
108 |
--------------------------------------------------------------------------------
/Source/IconImage/LightAppIcon.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Windows.Controls;
2 |
3 | namespace IconImage;
4 |
5 | public partial class LightAppIcon : UserControl
6 | {
7 | public LightAppIcon()
8 | {
9 | InitializeComponent();
10 | }
11 | }
--------------------------------------------------------------------------------
/Source/IconImage/MainWindow.xaml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
24 |
26 |
28 |
29 |
30 |
32 |
33 |
34 |
35 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
50 |
56 |
57 |
58 |
59 |
60 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/Source/IconImage/MainWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Media;
4 | using System.Windows;
5 | using Microsoft.Win32;
6 |
7 | namespace IconImage;
8 |
9 | public partial class MainWindow : Window
10 | {
11 | #region Property
12 |
13 | public double ImageLength
14 | {
15 | get { return (double)GetValue(ImageLengthProperty); }
16 | set { SetValue(ImageLengthProperty, value); }
17 | }
18 | public static readonly DependencyProperty ImageLengthProperty =
19 | DependencyProperty.Register(
20 | "ImageLength",
21 | typeof(double),
22 | typeof(MainWindow),
23 | new PropertyMetadata(256D));
24 |
25 | #endregion
26 |
27 | public MainWindow()
28 | {
29 | InitializeComponent();
30 | }
31 |
32 | private void Save(object sender, RoutedEventArgs e) => SaveImage();
33 |
34 | private string _fileName = "Icon.png";
35 | private string _folderPath;
36 |
37 | private void SaveImage()
38 | {
39 | var sfd = new SaveFileDialog
40 | {
41 | FileName = _fileName,
42 | Filter = "*(.png)|*.png",
43 | InitialDirectory = _folderPath ?? Environment.GetFolderPath(Environment.SpecialFolder.Desktop)
44 | };
45 |
46 | if (sfd.ShowDialog() != true)
47 | return;
48 |
49 | _fileName = Path.GetFileName(sfd.FileName);
50 | _folderPath = Path.GetDirectoryName(sfd.FileName);
51 |
52 | FrameworkElementImage.SaveImage(ImageContent, sfd.FileName, ImageLength, ImageLength);
53 |
54 | SystemSounds.Asterisk.Play();
55 | }
56 | }
--------------------------------------------------------------------------------
/Source/IconImage/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Resources;
3 | using System.Runtime.CompilerServices;
4 | using System.Runtime.InteropServices;
5 | using System.Windows;
6 |
7 | // General Information about an assembly is controlled through the following
8 | // set of attributes. Change these attribute values to modify the information
9 | // associated with an assembly.
10 | [assembly: AssemblyTitle("IconImage")]
11 | [assembly: AssemblyDescription("")]
12 | [assembly: AssemblyConfiguration("")]
13 | [assembly: AssemblyCompany("")]
14 | [assembly: AssemblyProduct("IconImage")]
15 | [assembly: AssemblyCopyright("Copyright © 2015 emoacht")]
16 | [assembly: AssemblyTrademark("")]
17 | [assembly: AssemblyCulture("")]
18 |
19 | // Setting ComVisible to false makes the types in this assembly not visible
20 | // to COM components. If you need to access a type in this assembly from
21 | // COM, set the ComVisible attribute to true on that type.
22 | [assembly: ComVisible(false)]
23 |
24 | //In order to begin building localizable applications, set
25 | //CultureYouAreCodingWith in your .csproj file
26 | //inside a . For example, if you are using US english
27 | //in your source files, set the to en-US. Then uncomment
28 | //the NeutralResourceLanguage attribute below. Update the "en-US" in
29 | //the line below to match the UICulture setting in the project file.
30 |
31 | //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
32 |
33 |
34 | [assembly: ThemeInfo(
35 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
36 | //(used if a resource is not found in the page,
37 | // or application resource dictionaries)
38 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
39 | //(used if a resource is not found in the page,
40 | // app, or any theme specific resource dictionaries)
41 | )]
42 |
43 |
44 | // Version information for an assembly consists of the following four values:
45 | //
46 | // Major Version
47 | // Minor Version
48 | // Build Number
49 | // Revision
50 | //
51 | // You can specify all the values or you can default the Build and Revision Numbers
52 | // by using the '*' as shown below:
53 | // [assembly: AssemblyVersion("1.0.*")]
54 | [assembly: AssemblyVersion("3.4.0.0")]
55 | [assembly: AssemblyFileVersion("3.4.0.0")]
56 |
--------------------------------------------------------------------------------
/Source/IconImage/Properties/Resources.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace IconImage.Properties {
12 | using System;
13 |
14 |
15 | ///
16 | /// A strongly-typed resource class, for looking up localized strings, etc.
17 | ///
18 | // This class was auto-generated by the StronglyTypedResourceBuilder
19 | // class via a tool like ResGen or Visual Studio.
20 | // To add or remove a member, edit your .ResX file then rerun ResGen
21 | // with the /str option, or rebuild your VS project.
22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
25 | internal class Resources {
26 |
27 | private static global::System.Resources.ResourceManager resourceMan;
28 |
29 | private static global::System.Globalization.CultureInfo resourceCulture;
30 |
31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
32 | internal Resources() {
33 | }
34 |
35 | ///
36 | /// Returns the cached ResourceManager instance used by this class.
37 | ///
38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
39 | internal static global::System.Resources.ResourceManager ResourceManager {
40 | get {
41 | if (object.ReferenceEquals(resourceMan, null)) {
42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("IconImage.Properties.Resources", typeof(Resources).Assembly);
43 | resourceMan = temp;
44 | }
45 | return resourceMan;
46 | }
47 | }
48 |
49 | ///
50 | /// Overrides the current thread's CurrentUICulture property for all
51 | /// resource lookups using this strongly typed resource class.
52 | ///
53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
54 | internal static global::System.Globalization.CultureInfo Culture {
55 | get {
56 | return resourceCulture;
57 | }
58 | set {
59 | resourceCulture = value;
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Source/IconImage/Properties/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | text/microsoft-resx
107 |
108 |
109 | 2.0
110 |
111 |
112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
113 |
114 |
115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
--------------------------------------------------------------------------------
/Source/IconImage/Properties/Settings.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace IconImage.Properties {
12 |
13 |
14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.3.0.0")]
16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
17 |
18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
19 |
20 | public static Settings Default {
21 | get {
22 | return defaultInstance;
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Source/IconImage/Properties/Settings.settings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/Source/IconImage/ScaleConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.Windows;
4 | using System.Windows.Data;
5 |
6 | namespace IconImage;
7 |
8 | [ValueConversion(typeof(double), typeof(double))]
9 | public class ScaleConverter : IValueConverter
10 | {
11 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
12 | {
13 | if ((value is not double sourceValue) || !double.TryParse(parameter?.ToString(), out double factor))
14 | return DependencyProperty.UnsetValue;
15 |
16 | return sourceValue / factor;
17 | }
18 |
19 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
20 | {
21 | if ((value is not double targetValue) || !double.TryParse(parameter?.ToString(), out double factor))
22 | return DependencyProperty.UnsetValue;
23 |
24 | return targetValue * factor;
25 | }
26 | }
--------------------------------------------------------------------------------
/Source/IconImage/UpdateImage.xaml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
14 |
15 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/Source/IconImage/UpdateImage.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Windows.Controls;
2 |
3 | namespace IconImage;
4 |
5 | public partial class UpdateImage : UserControl
6 | {
7 | public UpdateImage()
8 | {
9 | InitializeComponent();
10 | }
11 | }
--------------------------------------------------------------------------------
/Source/Installer/Installer.wixproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Wifinian.Installer
4 |
5 |
6 | Debug
7 | ICE69
8 |
9 |
10 | False
11 | ICE69
12 |
13 |
14 |
15 | Wifinian
16 | {056ef371-bf6b-42c3-82b7-a113cafa5718}
17 | True
18 | True
19 | Binaries;Content;Satellites
20 | INSTALLFOLDER
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/Source/Installer/Resources/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emoacht/Wifinian/e17e5989de2be66b3731dcf2a7dc88a505bd5e3f/Source/Installer/Resources/banner.png
--------------------------------------------------------------------------------
/Source/Installer/Resources/dialog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emoacht/Wifinian/e17e5989de2be66b3731dcf2a7dc88a505bd5e3f/Source/Installer/Resources/dialog.png
--------------------------------------------------------------------------------
/Source/ReactivePropertyTest/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/Source/ReactivePropertyTest/App.xaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Source/ReactivePropertyTest/App.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 |
3 | namespace ReactivePropertyTest;
4 |
5 | public partial class App : Application
6 | {
7 | }
--------------------------------------------------------------------------------
/Source/ReactivePropertyTest/BindableBase.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.ComponentModel;
3 | using System.Runtime.CompilerServices;
4 |
5 | namespace ReactivePropertyTest;
6 |
7 | public abstract class BindableBase : INotifyPropertyChanged
8 | {
9 | public event PropertyChangedEventHandler PropertyChanged;
10 |
11 | protected bool SetProperty(ref T storage, T value, [CallerMemberName] string propertyName = null)
12 | {
13 | if (EqualityComparer.Default.Equals(storage, value))
14 | return false;
15 |
16 | storage = value;
17 | OnPropertyChanged(propertyName);
18 | return true;
19 | }
20 |
21 | protected void OnPropertyChanged([CallerMemberName] string propertyName = null) =>
22 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
23 | }
--------------------------------------------------------------------------------
/Source/ReactivePropertyTest/MainWindow.xaml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
31 |
34 |
35 |
38 |
41 |
42 |
43 |
47 |
48 |
49 |
50 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
63 |
65 |
67 |
69 |
71 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
124 |
135 |
139 |
140 |
141 |
--------------------------------------------------------------------------------
/Source/ReactivePropertyTest/MainWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 |
3 | namespace ReactivePropertyTest;
4 |
5 | public partial class MainWindow : Window
6 | {
7 | public MainWindow()
8 | {
9 | InitializeComponent();
10 | }
11 | }
--------------------------------------------------------------------------------
/Source/ReactivePropertyTest/MemberViewModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.Reactive.Linq;
4 | using Reactive.Bindings;
5 |
6 | namespace ReactivePropertyTest;
7 |
8 | public class MemberViewModel : BindableBase
9 | {
10 | public string Name { get; }
11 |
12 | public bool IsLong
13 | {
14 | get => _isLong;
15 | set => SetProperty(ref _isLong, value);
16 | }
17 | private bool _isLong;
18 |
19 | public ReactivePropertySlim IsSelected { get; }
20 |
21 | public MemberViewModel(string name, bool isSelected = false)
22 | {
23 | this.Name = name ?? throw new ArgumentNullException(nameof(name));
24 |
25 | IsSelected = new ReactivePropertySlim(isSelected);
26 | IsSelected.Subscribe(x => Debug.WriteLine($"IsSelected: {x}"));
27 | }
28 | }
--------------------------------------------------------------------------------
/Source/ReactivePropertyTest/ObservableExtension.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Collections.ObjectModel;
4 | using System.Collections.Specialized;
5 | using System.Linq;
6 | using System.Linq.Expressions;
7 | using System.Reactive.Linq;
8 | using Reactive.Bindings.Extensions;
9 |
10 | namespace ReactivePropertyTest;
11 |
12 | public static class ObservableExtension
13 | {
14 | public static IObservable> ObserveElementBooleanObservableProperty(
15 | this ObservableCollection source,
16 | Expression>> propertySelector)
17 | where TElement : class
18 | {
19 | var instanceCache = new List();
20 | var propertySelectorDelegate = propertySelector.Compile();
21 |
22 | var elementPropertyChanged = source
23 | .ObserveElementObservableProperty(propertySelector)
24 | .Do(x =>
25 | {
26 | if (!x.Value)
27 | {
28 | instanceCache.Remove(x.Instance);
29 | }
30 | else if (!instanceCache.Contains(x.Instance))
31 | {
32 | instanceCache.Add(x.Instance);
33 | }
34 | })
35 | .Select(_ => instanceCache);
36 |
37 | var collectionChanged = source
38 | .CollectionChangedAsObservable()
39 | .Where(x => x.Action != NotifyCollectionChangedAction.Move)
40 | .Do(x =>
41 | {
42 | switch (x.Action)
43 | {
44 | case NotifyCollectionChangedAction.Add:
45 | case NotifyCollectionChangedAction.Remove:
46 | case NotifyCollectionChangedAction.Replace:
47 | if (x.OldItems is { Count: > 0 })
48 | {
49 | foreach (var instance in x.OldItems.Cast())
50 | {
51 | instanceCache.Remove(instance);
52 | }
53 | }
54 | if (x.NewItems is { Count: > 0 })
55 | {
56 | // This route is not really necessary because if the element value of
57 | // IObservable is true, ObserveElementObservableProperty method
58 | // will handle it.
59 |
60 | foreach (var instance in x.NewItems.Cast())
61 | {
62 | if (instanceCache.Contains(instance))
63 | continue;
64 |
65 | // If no element is sent from the sequence, nothing will happen.
66 | propertySelectorDelegate(instance)
67 | .Where(y => y)
68 | .Subscribe(_ => instanceCache.Add(instance))
69 | .Dispose();
70 | }
71 | }
72 | break;
73 | case NotifyCollectionChangedAction.Reset:
74 | instanceCache.Clear();
75 | break;
76 | }
77 | })
78 | .Select(_ => instanceCache);
79 |
80 | return Observable.Merge(elementPropertyChanged, collectionChanged);
81 | }
82 |
83 | public static IObservable> ObserveElementFilteredObservableProperty(
84 | this ObservableCollection source,
85 | Expression>> propertySelector,
86 | Func filter)
87 | where TElement : class
88 | {
89 | var instanceCache = new List();
90 | var propertySelectorDelegate = propertySelector.Compile();
91 |
92 | var elementPropertyChanged = source
93 | .ObserveElementObservableProperty(propertySelector)
94 | .Do(x =>
95 | {
96 | if (!filter(x.Value))
97 | {
98 | instanceCache.Remove(x.Instance);
99 | }
100 | else if (!instanceCache.Contains(x.Instance))
101 | {
102 | instanceCache.Add(x.Instance);
103 | }
104 | })
105 | .Select(_ => instanceCache);
106 |
107 | var collectionChanged = source
108 | .CollectionChangedAsObservable()
109 | .Where(x => x.Action != NotifyCollectionChangedAction.Move)
110 | .Do(x =>
111 | {
112 | switch (x.Action)
113 | {
114 | case NotifyCollectionChangedAction.Add:
115 | case NotifyCollectionChangedAction.Remove:
116 | case NotifyCollectionChangedAction.Replace:
117 | if (x.OldItems is { Count: > 0 })
118 | {
119 | foreach (var instance in x.OldItems.Cast())
120 | {
121 | instanceCache.Remove(instance);
122 | }
123 | }
124 | if (x.NewItems is { Count: > 0 })
125 | {
126 | // This route is not really necessary because if the return value of
127 | // Func is true, ObserveElementObservableProperty method
128 | // will handle it.
129 |
130 | foreach (var instance in x.NewItems.Cast())
131 | {
132 | if (instanceCache.Contains(instance))
133 | continue;
134 |
135 | // If no element is sent from the sequence, nothing will happen.
136 | propertySelectorDelegate(instance)
137 | .Where(y => filter(y))
138 | .Subscribe(_ => instanceCache.Add(instance))
139 | .Dispose();
140 | }
141 | }
142 | break;
143 | case NotifyCollectionChangedAction.Reset:
144 | instanceCache.Clear();
145 | break;
146 | }
147 | })
148 | .Select(_ => instanceCache);
149 |
150 | return Observable.Merge(elementPropertyChanged, collectionChanged);
151 | }
152 | }
--------------------------------------------------------------------------------
/Source/ReactivePropertyTest/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Resources;
3 | using System.Runtime.CompilerServices;
4 | using System.Runtime.InteropServices;
5 | using System.Windows;
6 |
7 | // General Information about an assembly is controlled through the following
8 | // set of attributes. Change these attribute values to modify the information
9 | // associated with an assembly.
10 | [assembly: AssemblyTitle("ReactiveProperty Test")]
11 | [assembly: AssemblyDescription("")]
12 | [assembly: AssemblyConfiguration("")]
13 | [assembly: AssemblyCompany("")]
14 | [assembly: AssemblyProduct("ReactiveProperty Test")]
15 | [assembly: AssemblyCopyright("Copyright © 2015 emoacht")]
16 | [assembly: AssemblyTrademark("")]
17 | [assembly: AssemblyCulture("")]
18 |
19 | // Setting ComVisible to false makes the types in this assembly not visible
20 | // to COM components. If you need to access a type in this assembly from
21 | // COM, set the ComVisible attribute to true on that type.
22 | [assembly: ComVisible(false)]
23 |
24 | //In order to begin building localizable applications, set
25 | //CultureYouAreCodingWith in your .csproj file
26 | //inside a . For example, if you are using US english
27 | //in your source files, set the to en-US. Then uncomment
28 | //the NeutralResourceLanguage attribute below. Update the "en-US" in
29 | //the line below to match the UICulture setting in the project file.
30 |
31 | //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
32 |
33 |
34 | [assembly: ThemeInfo(
35 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
36 | //(used if a resource is not found in the page,
37 | // or application resource dictionaries)
38 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
39 | //(used if a resource is not found in the page,
40 | // app, or any theme specific resource dictionaries)
41 | )]
42 |
43 |
44 | // Version information for an assembly consists of the following four values:
45 | //
46 | // Major Version
47 | // Minor Version
48 | // Build Number
49 | // Revision
50 | //
51 | // You can specify all the values or you can default the Build and Revision Numbers
52 | // by using the '*' as shown below:
53 | // [assembly: AssemblyVersion("1.0.*")]
54 | [assembly: AssemblyVersion("3.7.1.0")]
55 | [assembly: AssemblyFileVersion("3.7.1.0")]
56 |
--------------------------------------------------------------------------------
/Source/ReactivePropertyTest/Properties/Resources.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace ReactivePropertyTest.Properties {
12 | using System;
13 |
14 |
15 | ///
16 | /// A strongly-typed resource class, for looking up localized strings, etc.
17 | ///
18 | // This class was auto-generated by the StronglyTypedResourceBuilder
19 | // class via a tool like ResGen or Visual Studio.
20 | // To add or remove a member, edit your .ResX file then rerun ResGen
21 | // with the /str option, or rebuild your VS project.
22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
25 | internal class Resources {
26 |
27 | private static global::System.Resources.ResourceManager resourceMan;
28 |
29 | private static global::System.Globalization.CultureInfo resourceCulture;
30 |
31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
32 | internal Resources() {
33 | }
34 |
35 | ///
36 | /// Returns the cached ResourceManager instance used by this class.
37 | ///
38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
39 | internal static global::System.Resources.ResourceManager ResourceManager {
40 | get {
41 | if (object.ReferenceEquals(resourceMan, null)) {
42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ReactivePropertyTest.Properties.Resources", typeof(Resources).Assembly);
43 | resourceMan = temp;
44 | }
45 | return resourceMan;
46 | }
47 | }
48 |
49 | ///
50 | /// Overrides the current thread's CurrentUICulture property for all
51 | /// resource lookups using this strongly typed resource class.
52 | ///
53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
54 | internal static global::System.Globalization.CultureInfo Culture {
55 | get {
56 | return resourceCulture;
57 | }
58 | set {
59 | resourceCulture = value;
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Source/ReactivePropertyTest/Properties/Settings.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace ReactivePropertyTest.Properties {
12 |
13 |
14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.3.0.0")]
16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
17 |
18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
19 |
20 | public static Settings Default {
21 | get {
22 | return defaultInstance;
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Source/ReactivePropertyTest/Properties/Settings.settings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/Source/VisualStateTest/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/Source/VisualStateTest/App.xaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Source/VisualStateTest/App.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 |
3 | namespace VisualStateTest;
4 |
5 | public partial class App : Application
6 | {
7 | }
--------------------------------------------------------------------------------
/Source/VisualStateTest/MainWindow.xaml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 | Gold
10 |
11 |
12 |
13 |
17 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
32 |
33 |
34 |
35 |
36 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
66 |
67 |
--------------------------------------------------------------------------------
/Source/VisualStateTest/MainWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 |
3 | namespace VisualStateTest;
4 |
5 | public partial class MainWindow : Window
6 | {
7 | public MainWindow()
8 | {
9 | InitializeComponent();
10 | }
11 | }
--------------------------------------------------------------------------------
/Source/VisualStateTest/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Resources;
3 | using System.Runtime.CompilerServices;
4 | using System.Runtime.InteropServices;
5 | using System.Windows;
6 |
7 | // General Information about an assembly is controlled through the following
8 | // set of attributes. Change these attribute values to modify the information
9 | // associated with an assembly.
10 | [assembly: AssemblyTitle("VisualState Test")]
11 | [assembly: AssemblyDescription("")]
12 | [assembly: AssemblyConfiguration("")]
13 | [assembly: AssemblyCompany("")]
14 | [assembly: AssemblyProduct("VisualState Test")]
15 | [assembly: AssemblyCopyright("Copyright © 2015 emoacht")]
16 | [assembly: AssemblyTrademark("")]
17 | [assembly: AssemblyCulture("")]
18 |
19 | // Setting ComVisible to false makes the types in this assembly not visible
20 | // to COM components. If you need to access a type in this assembly from
21 | // COM, set the ComVisible attribute to true on that type.
22 | [assembly: ComVisible(false)]
23 |
24 | //In order to begin building localizable applications, set
25 | //CultureYouAreCodingWith in your .csproj file
26 | //inside a . For example, if you are using US english
27 | //in your source files, set the to en-US. Then uncomment
28 | //the NeutralResourceLanguage attribute below. Update the "en-US" in
29 | //the line below to match the UICulture setting in the project file.
30 |
31 | //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
32 |
33 |
34 | [assembly: ThemeInfo(
35 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
36 | //(used if a resource is not found in the page,
37 | // or application resource dictionaries)
38 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
39 | //(used if a resource is not found in the page,
40 | // app, or any theme specific resource dictionaries)
41 | )]
42 |
43 |
44 | // Version information for an assembly consists of the following four values:
45 | //
46 | // Major Version
47 | // Minor Version
48 | // Build Number
49 | // Revision
50 | //
51 | // You can specify all the values or you can default the Build and Revision Numbers
52 | // by using the '*' as shown below:
53 | // [assembly: AssemblyVersion("1.0.*")]
54 | [assembly: AssemblyVersion("3.5.0.0")]
55 | [assembly: AssemblyFileVersion("3.5.0.0")]
56 |
--------------------------------------------------------------------------------
/Source/VisualStateTest/Properties/Resources.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace VisualStateTest.Properties {
12 | using System;
13 |
14 |
15 | ///
16 | /// A strongly-typed resource class, for looking up localized strings, etc.
17 | ///
18 | // This class was auto-generated by the StronglyTypedResourceBuilder
19 | // class via a tool like ResGen or Visual Studio.
20 | // To add or remove a member, edit your .ResX file then rerun ResGen
21 | // with the /str option, or rebuild your VS project.
22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
25 | internal class Resources {
26 |
27 | private static global::System.Resources.ResourceManager resourceMan;
28 |
29 | private static global::System.Globalization.CultureInfo resourceCulture;
30 |
31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
32 | internal Resources() {
33 | }
34 |
35 | ///
36 | /// Returns the cached ResourceManager instance used by this class.
37 | ///
38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
39 | internal static global::System.Resources.ResourceManager ResourceManager {
40 | get {
41 | if (object.ReferenceEquals(resourceMan, null)) {
42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("VisualStateTest.Properties.Resources", typeof(Resources).Assembly);
43 | resourceMan = temp;
44 | }
45 | return resourceMan;
46 | }
47 | }
48 |
49 | ///
50 | /// Overrides the current thread's CurrentUICulture property for all
51 | /// resource lookups using this strongly typed resource class.
52 | ///
53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
54 | internal static global::System.Globalization.CultureInfo Culture {
55 | get {
56 | return resourceCulture;
57 | }
58 | set {
59 | resourceCulture = value;
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Source/VisualStateTest/Properties/Settings.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace VisualStateTest.Properties {
12 |
13 |
14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.3.0.0")]
16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
17 |
18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
19 |
20 | public static Settings Default {
21 | get {
22 | return defaultInstance;
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Source/VisualStateTest/Properties/Settings.settings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/Source/VisualStateTest/VisualStateBehavior.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Linq;
5 | using System.Windows;
6 | using System.Windows.Media;
7 | using System.Windows.Threading;
8 | using Microsoft.Xaml.Behaviors;
9 |
10 | namespace VisualStateTest;
11 |
12 | [TypeConstraint(typeof(FrameworkElement))]
13 | public class VisualStateBehavior : Behavior
14 | {
15 | public int Interval
16 | {
17 | get { return (int)GetValue(IntervalProperty); }
18 | set { SetValue(IntervalProperty, value); }
19 | }
20 | public static readonly DependencyProperty IntervalProperty =
21 | DependencyProperty.Register(
22 | "Interval",
23 | typeof(int),
24 | typeof(VisualStateBehavior),
25 | new FrameworkPropertyMetadata(3));
26 |
27 | private DispatcherTimer _timer;
28 |
29 | protected override void OnAttached()
30 | {
31 | base.OnAttached();
32 |
33 | _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(Interval) };
34 | _timer.Tick += (sender, e) => CheckVisualState(this.AssociatedObject);
35 | _timer.Start();
36 | }
37 |
38 | protected override void OnDetaching()
39 | {
40 | base.OnDetaching();
41 |
42 | _timer?.Stop();
43 | }
44 |
45 | private static void CheckVisualState(FrameworkElement element)
46 | {
47 | var groups = GetVisualStateGroups(element);
48 | if (groups is not null)
49 | {
50 | foreach (var group in groups)
51 | Debug.WriteLine($"Element: {element.Name} -> Group: {group.Name} -> State: {group.CurrentState?.Name}");
52 | }
53 | }
54 |
55 | private static IEnumerable GetVisualStateGroups(FrameworkElement element)
56 | {
57 | if (VisualTreeHelper.GetChildrenCount(element) <= 0) // If the ControlTemplate has not been applied yet
58 | return null;
59 |
60 | foreach (var descendant in GetDescendants(element).OfType())
61 | {
62 | var groups = VisualStateManager.GetVisualStateGroups(descendant)?.Cast();
63 | if (groups is not null)
64 | return groups;
65 | }
66 | return null;
67 | }
68 |
69 | private static IEnumerable GetDescendants(DependencyObject reference)
70 | {
71 | if (reference is null)
72 | yield break;
73 |
74 | var queue = new Queue();
75 |
76 | do
77 | {
78 | var parent = (queue.Count == 0) ? reference : queue.Dequeue();
79 |
80 | var count = VisualTreeHelper.GetChildrenCount(parent);
81 | for (int i = 0; i < count; i++)
82 | {
83 | var child = VisualTreeHelper.GetChild(parent, i);
84 | queue.Enqueue(child);
85 |
86 | yield return child;
87 | }
88 | }
89 | while (queue.Count > 0);
90 | }
91 | }
--------------------------------------------------------------------------------
/Source/VisualStateTest/VisualStateMonitor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 | using System.Windows;
7 | using System.Windows.Media;
8 |
9 | namespace VisualStateTest;
10 |
11 | public class VisualStateMonitor : DependencyObject
12 | {
13 | public static double GetInterval(DependencyObject obj)
14 | {
15 | return (double)obj.GetValue(IntervalProperty);
16 | }
17 | public static void SetInterval(DependencyObject obj, double value)
18 | {
19 | obj.SetValue(IntervalProperty, value);
20 | }
21 | public static readonly DependencyProperty IntervalProperty =
22 | DependencyProperty.RegisterAttached(
23 | "Interval",
24 | typeof(double),
25 | typeof(VisualStateMonitor),
26 | new FrameworkPropertyMetadata(0D, OnIntervalChanged));
27 |
28 | private static void OnIntervalChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
29 | {
30 | if (d is not FrameworkElement element)
31 | return;
32 |
33 | var interval = (double)e.NewValue;
34 | if (interval <= 0D)
35 | return;
36 |
37 | Task.Run(async () =>
38 | {
39 | while (true)
40 | {
41 | element.Dispatcher.Invoke(() => CheckVisualState(element));
42 |
43 | await Task.Delay(TimeSpan.FromSeconds(interval));
44 | }
45 | });
46 | }
47 |
48 | private static void CheckVisualState(FrameworkElement element)
49 | {
50 | var groups = GetVisualStateGroups(element);
51 | if (groups is not null)
52 | {
53 | foreach (var group in groups)
54 | Debug.WriteLine($"Element: {element.Name} -> Group: {group.Name} -> State: {group.CurrentState?.Name}");
55 | }
56 | }
57 |
58 | private static IEnumerable GetVisualStateGroups(FrameworkElement element)
59 | {
60 | if (VisualTreeHelper.GetChildrenCount(element) <= 0) // If the ControlTemplate has not been applied yet
61 | return null;
62 |
63 | foreach (var descendant in GetDescendants(element).OfType())
64 | {
65 | var groups = VisualStateManager.GetVisualStateGroups(descendant)?.Cast();
66 | if (groups is not null)
67 | return groups;
68 | }
69 | return null;
70 | }
71 |
72 | private static IEnumerable GetDescendants(DependencyObject reference)
73 | {
74 | if (reference is null)
75 | yield break;
76 |
77 | var queue = new Queue();
78 |
79 | do
80 | {
81 | var parent = (queue.Count == 0) ? reference : queue.Dequeue();
82 |
83 | var count = VisualTreeHelper.GetChildrenCount(parent);
84 | for (int i = 0; i < count; i++)
85 | {
86 | var child = VisualTreeHelper.GetChild(parent, i);
87 | queue.Enqueue(child);
88 |
89 | yield return child;
90 | }
91 | }
92 | while (queue.Count > 0);
93 | }
94 | }
--------------------------------------------------------------------------------
/Source/VisualStateTest/VisualStateTest.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {BCE9DA19-3C0D-4F17-8927-1320CF13CF2F}
8 | WinExe
9 | Properties
10 | VisualStateTest
11 | VisualStateTest
12 | v4.8
13 | 512
14 | {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
15 | 4
16 | true
17 |
18 |
19 |
20 | AnyCPU
21 | true
22 | full
23 | false
24 | bin\Debug\
25 | DEBUG;TRACE
26 | prompt
27 | 4
28 |
29 |
30 | AnyCPU
31 | pdbonly
32 | true
33 | bin\Release\
34 | TRACE
35 | prompt
36 | 4
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | 4.0
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | MSBuild:Compile
55 | Designer
56 |
57 |
58 |
59 |
60 | MSBuild:Compile
61 | Designer
62 |
63 |
64 | App.xaml
65 | Code
66 |
67 |
68 | MainWindow.xaml
69 | Code
70 |
71 |
72 |
73 |
74 | Code
75 |
76 |
77 | True
78 | True
79 | Resources.resx
80 |
81 |
82 | True
83 | Settings.settings
84 | True
85 |
86 |
87 | ResXFileCodeGenerator
88 | Resources.Designer.cs
89 |
90 |
91 | SettingsSingleFileGenerator
92 | Settings.Designer.cs
93 |
94 |
95 |
96 |
97 |
98 | Designer
99 |
100 |
101 |
102 |
103 | 1.1.135
104 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/Source/Wifinian.Test/LanguageServiceTest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text.RegularExpressions;
3 | using Microsoft.VisualStudio.TestTools.UnitTesting;
4 |
5 | using Wifinian.Models;
6 |
7 | namespace Wifinian.Test;
8 |
9 | [TestClass]
10 | public class LanguageServiceTest
11 | {
12 | [TestMethod]
13 | public void TestLanguageFiles()
14 | {
15 | Assert.IsTrue(TestLanguageFilePattern("language.en.txt", "en"));
16 | Assert.IsTrue(TestLanguageFilePattern("language.ja.txt", "ja"));
17 | Assert.IsTrue(TestLanguageFilePattern("language.ja-JP.txt", "ja-JP"));
18 | Assert.IsTrue(TestLanguageFilePattern("language.zh-Hans-HK.txt", "zh-Hans-HK"));
19 |
20 | Assert.IsTrue(TestLanguageFilePattern("language_en", "en"));
21 | Assert.IsTrue(TestLanguageFilePattern("language_ja", "ja"));
22 | Assert.IsTrue(TestLanguageFilePattern("language_ja_JP", "ja_JP"));
23 | Assert.IsTrue(TestLanguageFilePattern("language_zh_Hans_HK", "zh_Hans_HK"));
24 | }
25 |
26 | #region Base
27 |
28 | private static Regex _languageFilePattern;
29 |
30 | [ClassInitialize]
31 | public static void InitializeLanguageFilePattern(TestContext context)
32 | {
33 | var @class = new PrivateType(typeof(LanguageService));
34 | _languageFilePattern = @class.GetStaticField("_languageFilePattern") as Regex;
35 | }
36 |
37 | private static bool TestLanguageFilePattern(string source, string expectedName)
38 | {
39 | var match = _languageFilePattern.Match(source);
40 | if (!match.Success)
41 | return false;
42 |
43 | var actualName = match.Groups["name"].Value;
44 | return string.Equals(expectedName, actualName, StringComparison.Ordinal);
45 | }
46 |
47 | #endregion
48 | }
--------------------------------------------------------------------------------
/Source/Wifinian.Test/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | [assembly: AssemblyTitle("Wifinian.Test")]
6 | [assembly: AssemblyDescription("")]
7 | [assembly: AssemblyConfiguration("")]
8 | [assembly: AssemblyCompany("")]
9 | [assembly: AssemblyProduct("Wifinian.Test")]
10 | [assembly: AssemblyCopyright("Copyright © 2021 emoacht")]
11 | [assembly: AssemblyTrademark("")]
12 | [assembly: AssemblyCulture("")]
13 |
14 | [assembly: ComVisible(false)]
15 |
16 | [assembly: Guid("98c08e30-5716-4bca-99f6-80413d56867b")]
17 |
18 | // [assembly: AssemblyVersion("1.0.*")]
19 | [assembly: AssemblyVersion("3.7.1.0")]
20 | [assembly: AssemblyFileVersion("3.7.1.0")]
21 |
--------------------------------------------------------------------------------
/Source/Wifinian.Test/Wifinian.Test.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {98C08E30-5716-4BCA-99F6-80413D56867B}
8 | Library
9 | Properties
10 | Wifinian.Test
11 | Wifinian.Test
12 | v4.8
13 | 512
14 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
15 | 15.0
16 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
17 | $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages
18 | False
19 | UnitTest
20 |
21 |
22 |
23 |
24 | true
25 | full
26 | false
27 | bin\Debug\
28 | DEBUG;TRACE
29 | prompt
30 | 4
31 |
32 |
33 | pdbonly
34 | true
35 | bin\Release\
36 | TRACE
37 | prompt
38 | 4
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | {056ef371-bf6b-42c3-82b7-a113cafa5718}
51 | Wifinian
52 |
53 |
54 |
55 |
56 | 2.2.10
57 |
58 |
59 | 2.2.10
60 |
61 |
62 | 9.7.0
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/Source/Wifinian.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.8.34330.188
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wifinian", "Wifinian\Wifinian.csproj", "{056EF371-BF6B-42C3-82B7-A113CAFA5718}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisualStateTest", "VisualStateTest\VisualStateTest.csproj", "{BCE9DA19-3C0D-4F17-8927-1320CF13CF2F}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactivePropertyTest", "ReactivePropertyTest\ReactivePropertyTest.csproj", "{2010B70C-7FEF-4C71-9E96-E09860401833}"
11 | EndProject
12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IconImage", "IconImage\IconImage.csproj", "{566D20ED-A863-43BA-BC28-D21E46FEC661}"
13 | EndProject
14 | Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "Installer", "Installer\Installer.wixproj", "{053F50A8-E2C9-4F92-BA1B-B8F71603585F}"
15 | EndProject
16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wifinian.Test", "Wifinian.Test\Wifinian.Test.csproj", "{98C08E30-5716-4BCA-99F6-80413D56867B}"
17 | EndProject
18 | Global
19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
20 | Debug|Any CPU = Debug|Any CPU
21 | Debug|x86 = Debug|x86
22 | Release|Any CPU = Release|Any CPU
23 | Release|x86 = Release|x86
24 | EndGlobalSection
25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
26 | {056EF371-BF6B-42C3-82B7-A113CAFA5718}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {056EF371-BF6B-42C3-82B7-A113CAFA5718}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {056EF371-BF6B-42C3-82B7-A113CAFA5718}.Debug|x86.ActiveCfg = Debug|Any CPU
29 | {056EF371-BF6B-42C3-82B7-A113CAFA5718}.Debug|x86.Build.0 = Debug|Any CPU
30 | {056EF371-BF6B-42C3-82B7-A113CAFA5718}.Release|Any CPU.ActiveCfg = Release|Any CPU
31 | {056EF371-BF6B-42C3-82B7-A113CAFA5718}.Release|Any CPU.Build.0 = Release|Any CPU
32 | {056EF371-BF6B-42C3-82B7-A113CAFA5718}.Release|x86.ActiveCfg = Release|Any CPU
33 | {056EF371-BF6B-42C3-82B7-A113CAFA5718}.Release|x86.Build.0 = Release|Any CPU
34 | {BCE9DA19-3C0D-4F17-8927-1320CF13CF2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
35 | {BCE9DA19-3C0D-4F17-8927-1320CF13CF2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
36 | {BCE9DA19-3C0D-4F17-8927-1320CF13CF2F}.Debug|x86.ActiveCfg = Debug|Any CPU
37 | {BCE9DA19-3C0D-4F17-8927-1320CF13CF2F}.Debug|x86.Build.0 = Debug|Any CPU
38 | {BCE9DA19-3C0D-4F17-8927-1320CF13CF2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
39 | {BCE9DA19-3C0D-4F17-8927-1320CF13CF2F}.Release|Any CPU.Build.0 = Release|Any CPU
40 | {BCE9DA19-3C0D-4F17-8927-1320CF13CF2F}.Release|x86.ActiveCfg = Release|Any CPU
41 | {BCE9DA19-3C0D-4F17-8927-1320CF13CF2F}.Release|x86.Build.0 = Release|Any CPU
42 | {2010B70C-7FEF-4C71-9E96-E09860401833}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
43 | {2010B70C-7FEF-4C71-9E96-E09860401833}.Debug|Any CPU.Build.0 = Debug|Any CPU
44 | {2010B70C-7FEF-4C71-9E96-E09860401833}.Debug|x86.ActiveCfg = Debug|Any CPU
45 | {2010B70C-7FEF-4C71-9E96-E09860401833}.Debug|x86.Build.0 = Debug|Any CPU
46 | {2010B70C-7FEF-4C71-9E96-E09860401833}.Release|Any CPU.ActiveCfg = Release|Any CPU
47 | {2010B70C-7FEF-4C71-9E96-E09860401833}.Release|Any CPU.Build.0 = Release|Any CPU
48 | {2010B70C-7FEF-4C71-9E96-E09860401833}.Release|x86.ActiveCfg = Release|Any CPU
49 | {2010B70C-7FEF-4C71-9E96-E09860401833}.Release|x86.Build.0 = Release|Any CPU
50 | {566D20ED-A863-43BA-BC28-D21E46FEC661}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
51 | {566D20ED-A863-43BA-BC28-D21E46FEC661}.Debug|Any CPU.Build.0 = Debug|Any CPU
52 | {566D20ED-A863-43BA-BC28-D21E46FEC661}.Debug|x86.ActiveCfg = Debug|Any CPU
53 | {566D20ED-A863-43BA-BC28-D21E46FEC661}.Debug|x86.Build.0 = Debug|Any CPU
54 | {566D20ED-A863-43BA-BC28-D21E46FEC661}.Release|Any CPU.ActiveCfg = Release|Any CPU
55 | {566D20ED-A863-43BA-BC28-D21E46FEC661}.Release|Any CPU.Build.0 = Release|Any CPU
56 | {566D20ED-A863-43BA-BC28-D21E46FEC661}.Release|x86.ActiveCfg = Release|Any CPU
57 | {566D20ED-A863-43BA-BC28-D21E46FEC661}.Release|x86.Build.0 = Release|Any CPU
58 | {053F50A8-E2C9-4F92-BA1B-B8F71603585F}.Debug|Any CPU.ActiveCfg = Debug|x86
59 | {053F50A8-E2C9-4F92-BA1B-B8F71603585F}.Debug|x86.ActiveCfg = Debug|x86
60 | {053F50A8-E2C9-4F92-BA1B-B8F71603585F}.Debug|x86.Build.0 = Debug|x86
61 | {053F50A8-E2C9-4F92-BA1B-B8F71603585F}.Release|Any CPU.ActiveCfg = Release|x86
62 | {053F50A8-E2C9-4F92-BA1B-B8F71603585F}.Release|x86.ActiveCfg = Release|x86
63 | {053F50A8-E2C9-4F92-BA1B-B8F71603585F}.Release|x86.Build.0 = Release|x86
64 | {98C08E30-5716-4BCA-99F6-80413D56867B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
65 | {98C08E30-5716-4BCA-99F6-80413D56867B}.Debug|Any CPU.Build.0 = Debug|Any CPU
66 | {98C08E30-5716-4BCA-99F6-80413D56867B}.Debug|x86.ActiveCfg = Debug|Any CPU
67 | {98C08E30-5716-4BCA-99F6-80413D56867B}.Debug|x86.Build.0 = Debug|Any CPU
68 | {98C08E30-5716-4BCA-99F6-80413D56867B}.Release|Any CPU.ActiveCfg = Release|Any CPU
69 | {98C08E30-5716-4BCA-99F6-80413D56867B}.Release|Any CPU.Build.0 = Release|Any CPU
70 | {98C08E30-5716-4BCA-99F6-80413D56867B}.Release|x86.ActiveCfg = Release|Any CPU
71 | {98C08E30-5716-4BCA-99F6-80413D56867B}.Release|x86.Build.0 = Release|Any CPU
72 | EndGlobalSection
73 | GlobalSection(SolutionProperties) = preSolution
74 | HideSolutionNode = FALSE
75 | EndGlobalSection
76 | GlobalSection(ExtensibilityGlobals) = postSolution
77 | SolutionGuid = {6E1FC128-E790-478C-B7CD-5B1E11D5DAEA}
78 | EndGlobalSection
79 | EndGlobal
80 |
--------------------------------------------------------------------------------
/Source/Wifinian/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/Source/Wifinian/App.xaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Source/Wifinian/App.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 |
3 | namespace Wifinian;
4 |
5 | public partial class App : Application
6 | {
7 | private AppKeeper _keeper;
8 | private AppController _controller;
9 |
10 | protected override async void OnStartup(StartupEventArgs e)
11 | {
12 | base.OnStartup(e);
13 |
14 | _keeper = new AppKeeper(e);
15 | if (!_keeper.Start())
16 | {
17 | this.Shutdown(0); // This shutdown is expected behavior.
18 | return;
19 | }
20 |
21 | _controller = new AppController(_keeper);
22 | await _controller.InitiateAsync();
23 |
24 | //this.MainWindow = new MainWindow();
25 | //this.MainWindow.Show();
26 | }
27 |
28 | protected override void OnExit(ExitEventArgs e)
29 | {
30 | _controller?.Dispose();
31 | _keeper?.Dispose();
32 |
33 | base.OnExit(e);
34 | }
35 | }
--------------------------------------------------------------------------------
/Source/Wifinian/Appkeeper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Reactive.Linq;
4 | using System.Threading.Tasks;
5 | using System.Windows;
6 | using System.Windows.Threading;
7 | using Reactive.Bindings.Extensions;
8 |
9 | using StartupAgency;
10 | using Wifinian.Common;
11 | using Wifinian.Models;
12 |
13 | namespace Wifinian;
14 |
15 | public class AppKeeper : DisposableBase
16 | {
17 | public StartupAgent StartupAgent { get; }
18 |
19 | public AppKeeper(StartupEventArgs e)
20 | {
21 | StartupAgent = new StartupAgent();
22 | }
23 |
24 | public bool Start()
25 | {
26 | #region Exceptions
27 |
28 | var dispatcherUnhandled = Observable.FromEventPattern(
29 | h => Application.Current.DispatcherUnhandledException += h,
30 | h => Application.Current.DispatcherUnhandledException -= h)
31 | .Select(x => x.EventArgs.Exception);
32 | var taskUnobserved = Observable.FromEventPattern(
33 | h => TaskScheduler.UnobservedTaskException += h,
34 | h => TaskScheduler.UnobservedTaskException -= h)
35 | .SelectMany(x => x.EventArgs.Exception.InnerExceptions);
36 | var appDomainUnhandled = Observable.FromEventPattern(
37 | h => AppDomain.CurrentDomain.UnhandledException += h,
38 | h => AppDomain.CurrentDomain.UnhandledException -= h)
39 | .Select(x => (Exception)x.EventArgs.ExceptionObject);
40 | Observable.Merge(dispatcherUnhandled, taskUnobserved, appDomainUnhandled)
41 | .Take(1)
42 | .Subscribe(x =>
43 | {
44 | if (LocationInfo.PromptOpenLocation(x))
45 | return;
46 |
47 | Logger.RecordException(x);
48 | })
49 | .AddTo(this.Subscription);
50 |
51 | #endregion
52 |
53 | var (success, _) = StartupAgent.Start(ProductInfo.Product, ProductInfo.StartupTaskId, null);
54 | return success;
55 | }
56 | }
--------------------------------------------------------------------------------
/Source/Wifinian/Common/BindableBase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.ComponentModel;
4 | using System.Runtime.CompilerServices;
5 |
6 | namespace Wifinian.Common;
7 |
8 | public abstract class BindableBase : INotifyPropertyChanged
9 | {
10 | public event PropertyChangedEventHandler PropertyChanged;
11 |
12 | protected virtual bool SetProperty(ref T storage, T value, T min, T max, [CallerMemberName] string propertyName = null)
13 | where T : struct, IComparable
14 | {
15 | if (0 <= min.CompareTo(max))
16 | throw new ArgumentException($"{nameof(min)} must be smaller than {nameof(max)}.");
17 |
18 | T normalize(T x) => (x.CompareTo(min) <= 0)
19 | ? min
20 | : (0 <= x.CompareTo(max))
21 | ? max
22 | : x;
23 |
24 | return SetProperty(ref storage, value, normalize, propertyName);
25 | }
26 |
27 | protected virtual bool SetProperty(ref T storage, T value, Func normalize, [CallerMemberName] string propertyName = null)
28 | {
29 | if (normalize is null)
30 | throw new ArgumentNullException(nameof(normalize));
31 |
32 | return SetProperty(ref storage, normalize.Invoke(value), propertyName);
33 | }
34 |
35 | protected virtual bool SetProperty(ref T storage, T value, [CallerMemberName] string propertyName = null)
36 | {
37 | if (EqualityComparer.Default.Equals(storage, value))
38 | return false;
39 |
40 | storage = value;
41 | OnPropertyChanged(propertyName);
42 | return true;
43 | }
44 |
45 | protected virtual bool SetProperty((Func get, Action set) accessor, T value, [CallerMemberName] string propertyName = null)
46 | {
47 | if (EqualityComparer.Default.Equals(accessor.get.Invoke(), value))
48 | return false;
49 |
50 | accessor.set.Invoke(value);
51 | OnPropertyChanged(propertyName);
52 | return true;
53 | }
54 |
55 | protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) =>
56 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
57 | }
--------------------------------------------------------------------------------
/Source/Wifinian/Common/DisposableBase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reactive.Disposables;
3 |
4 | namespace Wifinian.Common;
5 |
6 | public abstract class DisposableBase : BindableBase, IDisposable
7 | {
8 | protected CompositeDisposable Subscription => _subscription.Value;
9 | private readonly Lazy _subscription = new(() => []);
10 |
11 | #region Dispose
12 |
13 | private bool _disposed = false;
14 |
15 | public void Dispose()
16 | {
17 | Dispose(true);
18 | GC.SuppressFinalize(this);
19 | }
20 |
21 | protected virtual void Dispose(bool disposing)
22 | {
23 | if (_disposed)
24 | return;
25 |
26 | if (disposing)
27 | {
28 | if (_subscription.IsValueCreated)
29 | _subscription.Value.Dispose();
30 | }
31 |
32 | _disposed = true;
33 | }
34 |
35 | #endregion
36 | }
--------------------------------------------------------------------------------
/Source/Wifinian/Helper/ColorExtension.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Windows.Media;
3 | using static System.Math;
4 |
5 | namespace Wifinian.Helper;
6 |
7 | ///
8 | /// Extension methods for
9 | ///
10 | public static class ColorExtension
11 | {
12 | public static HsbColor ToAhsb(this Color source) =>
13 | HsbColor.FromArgb(source);
14 |
15 | ///
16 | /// Gets Color brightened by a specified factor.
17 | ///
18 | /// Source Color
19 | /// Factor
20 | /// Brightened Color
21 | public static Color ToBrightened(this Color source, float factor)
22 | {
23 | if (factor <= 0F)
24 | throw new ArgumentOutOfRangeException(nameof(factor), factor, "The factor must be positive.");
25 |
26 | var bridgeColor = HsbColor.FromArgb(source);
27 | bridgeColor.B = Min(1F, bridgeColor.B * factor);
28 |
29 | return bridgeColor.ToArgb();
30 | }
31 |
32 | ///
33 | /// Gets Color equivalent to translucent Color with white background.
34 | ///
35 | /// Source Color
36 | /// Opaque Color
37 | public static Color ToOpaque(this Color source)
38 | {
39 | var alpha = source.A;
40 | byte Compute(byte value) => (byte)(byte.MaxValue - (byte.MaxValue - value) * alpha / (float)byte.MaxValue);
41 |
42 | return Color.FromRgb(Compute(source.R), Compute(source.G), Compute(source.B));
43 | }
44 | }
--------------------------------------------------------------------------------
/Source/Wifinian/Helper/HsbColor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Windows.Media;
3 | using static System.Math;
4 |
5 | namespace Wifinian.Helper;
6 |
7 | ///
8 | /// HSB Color
9 | ///
10 | public struct HsbColor
11 | {
12 | #region Property
13 |
14 | ///
15 | /// Alpha
16 | ///
17 | public byte A { get; set; }
18 |
19 | ///
20 | /// Hue
21 | ///
22 | public float H
23 | {
24 | get => _h;
25 | set
26 | {
27 | if (value is < 0F or >= 360F)
28 | throw new ArgumentOutOfRangeException(nameof(value), value, "The range of hue: 0.0 <= value < 360.0");
29 |
30 | _h = value;
31 | }
32 | }
33 | private float _h;
34 |
35 | ///
36 | /// Saturation
37 | ///
38 | public float S
39 | {
40 | get => _s;
41 | set
42 | {
43 | if (value is < 0F or > 1F)
44 | throw new ArgumentOutOfRangeException(nameof(value), value, "The range of saturation: 0.0 <= value <= 1.0");
45 |
46 | _s = value;
47 | }
48 | }
49 | private float _s;
50 |
51 | ///
52 | /// Brightness
53 | ///
54 | public float B
55 | {
56 | get => _b;
57 | set
58 | {
59 | if (value is < 0F or > 1F)
60 | throw new ArgumentOutOfRangeException(nameof(value), value, "The range of brightness: 0.0 <= value <= 1.0");
61 |
62 | _b = value;
63 | }
64 | }
65 | private float _b;
66 |
67 | #endregion
68 |
69 | #region Convert
70 |
71 | public static HsbColor FromRgb(float hue, float saturation, float brightness) =>
72 | FromArgb(255, hue, saturation, brightness);
73 |
74 | public static HsbColor FromArgb(byte alpha, float hue, float saturation, float brightness) =>
75 | new HsbColor { A = alpha, H = hue, S = saturation, B = brightness };
76 |
77 | public static HsbColor FromArgb(Color source)
78 | {
79 | float fr = source.R / 255F;
80 | float fg = source.G / 255F;
81 | float fb = source.B / 255F;
82 |
83 | float max = Max(Max(fr, fg), fb);
84 | float min = Min(Min(fr, fg), fb);
85 |
86 | float c = max - min;
87 |
88 | float h;
89 | if (c == 0F)
90 | {
91 | h = 0F;
92 | }
93 | else if (max == fr)
94 | {
95 | h = (fg - fb) / c;
96 | }
97 | else if (max == fg)
98 | {
99 | h = 2f + (fb - fr) / c;
100 | }
101 | else
102 | {
103 | h = 4f + (fr - fg) / c;
104 | }
105 | h *= 60F;
106 | if (h < 0F)
107 | {
108 | h += 360F;
109 | }
110 |
111 | float s;
112 | if (max == 0F)
113 | {
114 | s = 0F;
115 | }
116 | else
117 | {
118 | s = c / max;
119 | }
120 |
121 | float b = max;
122 |
123 | return new HsbColor { A = source.A, H = h, S = s, B = b };
124 | }
125 |
126 | public static Color ToArgb(HsbColor source)
127 | {
128 | float fr = source.B;
129 | float fg = source.B;
130 | float fb = source.B;
131 |
132 | if (0F < source.S)
133 | {
134 | float h = source.H / 60F;
135 | int i = (int)Floor(h);
136 | float f = h - i;
137 |
138 | float p = source.B * (1F - source.S);
139 | float q = source.B * (1F - source.S * f);
140 | float t = source.B * (1F - source.S * (1F - f));
141 |
142 | switch (i)
143 | {
144 | case 0:
145 | fg = t;
146 | fb = p;
147 | break;
148 | case 1:
149 | fr = q;
150 | fb = p;
151 | break;
152 | case 2:
153 | fr = p;
154 | fb = t;
155 | break;
156 | case 3:
157 | fr = p;
158 | fg = q;
159 | break;
160 | case 4:
161 | fr = t;
162 | fg = p;
163 | break;
164 | case 5:
165 | fg = p;
166 | fb = q;
167 | break;
168 | default:
169 | throw new InvalidOperationException(); // This must not happen.
170 | }
171 | }
172 |
173 | byte r = (byte)Max(Min(Round(fr * 255F), 255), 0);
174 | byte g = (byte)Max(Min(Round(fg * 255F), 255), 0);
175 | byte b = (byte)Max(Min(Round(fb * 255F), 255), 0);
176 |
177 | return Color.FromArgb(source.A, r, g, b);
178 | }
179 |
180 | public Color ToArgb() => ToArgb(this);
181 |
182 | #endregion
183 |
184 | #region Other
185 |
186 | public override string ToString() => $"{{A={A},H={H},S={S},B={B}}}";
187 |
188 | #endregion
189 | }
--------------------------------------------------------------------------------
/Source/Wifinian/Helper/OsVersion.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Runtime.CompilerServices;
4 |
5 | namespace Wifinian.Helper;
6 |
7 | ///
8 | /// OS version information
9 | ///
10 | internal static class OsVersion
11 | {
12 | ///
13 | /// Whether OS is Windows 7 (6.1) or greater
14 | ///
15 | public static bool Is7OrGreater => IsEqualToOrGreaterThan(6, 1);
16 |
17 | ///
18 | /// Whether OS is Windows 8 (6.2) or greater
19 | ///
20 | public static bool Is8OrGreater => IsEqualToOrGreaterThan(6, 2);
21 |
22 | ///
23 | /// Whether OS is Windows 8.1 (6.3) or greater
24 | ///
25 | public static bool Is8Point1OrGreater => IsEqualToOrGreaterThan(6, 3);
26 |
27 | ///
28 | /// Whether OS is Windows 10 (10.0.10240) or greater
29 | ///
30 | public static bool Is10OrGreater => IsEqualToOrGreaterThan(10, 0, 10240);
31 |
32 | ///
33 | /// Whether OS is Windows 11 (10.0.22000) or greater
34 | ///
35 | public static bool Is11OrGreater => IsEqualToOrGreaterThan(10, 0, 22000);
36 |
37 | ///
38 | /// Whether OS is Windows 11 (10.0.26100) or greater
39 | ///
40 | public static bool Is11Build26100OrGreater => IsEqualToOrGreaterThan(10, 0, 26100);
41 |
42 | #region Cache
43 |
44 | private static readonly Dictionary _cache = [];
45 | private static readonly object _lock = new();
46 |
47 | private static bool IsEqualToOrGreaterThan(int major, int minor = 0, int build = 0, [CallerMemberName] string propertyName = null)
48 | {
49 | lock (_lock)
50 | {
51 | if (!_cache.TryGetValue(propertyName, out bool value))
52 | {
53 | value = (new Version(major, minor, build) <= Environment.OSVersion.Version);
54 | _cache[propertyName] = value;
55 | }
56 | return value;
57 | }
58 | }
59 |
60 | #endregion
61 | }
--------------------------------------------------------------------------------
/Source/Wifinian/Library/ScreenFrame.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emoacht/Wifinian/e17e5989de2be66b3731dcf2a7dc88a505bd5e3f/Source/Wifinian/Library/ScreenFrame.dll
--------------------------------------------------------------------------------
/Source/Wifinian/Library/StartupAgency.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emoacht/Wifinian/e17e5989de2be66b3731dcf2a7dc88a505bd5e3f/Source/Wifinian/Library/StartupAgency.dll
--------------------------------------------------------------------------------
/Source/Wifinian/Models/AppDataService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Linq;
4 | using System.Reflection;
5 | using System.Xml.Serialization;
6 |
7 | namespace Wifinian.Models;
8 |
9 | internal static class AppDataService
10 | {
11 | public static string FolderPath
12 | {
13 | get
14 | {
15 | if (_folderPath is null)
16 | {
17 | var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
18 | if (string.IsNullOrEmpty(appDataPath)) // This should not happen.
19 | throw new DirectoryNotFoundException();
20 |
21 | _folderPath = Path.Combine(appDataPath, ProductInfo.Product);
22 | }
23 | return _folderPath;
24 | }
25 | }
26 | private static string _folderPath;
27 |
28 | public static string EnsureFolderPath()
29 | {
30 | if (!Directory.Exists(FolderPath))
31 | Directory.CreateDirectory(FolderPath);
32 |
33 | return FolderPath;
34 | }
35 |
36 | public static void Load(T instance, string fileName) where T : class
37 | {
38 | var filePath = Path.Combine(FolderPath, fileName);
39 | var fileInfo = new FileInfo(filePath);
40 | if (!fileInfo.Exists || (fileInfo.Length == 0))
41 | return;
42 |
43 | using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
44 |
45 | var type = instance.GetType(); // GetType method works in derived class.
46 | var serializer = new XmlSerializer(type);
47 | var loaded = (T)serializer.Deserialize(fs);
48 |
49 | type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
50 | .Where(x => x.CanWrite)
51 | .ToList()
52 | .ForEach(x => x.SetValue(instance, x.GetValue(loaded)));
53 | }
54 |
55 | public static void Save(T instance, string fileName) where T : class
56 | {
57 | EnsureFolderPath();
58 | var filePath = Path.Combine(FolderPath, fileName);
59 |
60 | using var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write);
61 |
62 | var type = instance.GetType(); // GetType method works in derived class.
63 | var serializer = new XmlSerializer(type);
64 | serializer.Serialize(fs, instance);
65 | }
66 | }
--------------------------------------------------------------------------------
/Source/Wifinian/Models/LanguageService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Generic;
4 | using System.Globalization;
5 | using System.IO;
6 | using System.Linq;
7 | using System.Reflection;
8 | using System.Runtime.CompilerServices;
9 | using System.Text.RegularExpressions;
10 | using System.Threading.Tasks;
11 |
12 | namespace Wifinian.Models;
13 |
14 | internal static class LanguageService
15 | {
16 | #region Content
17 |
18 | // Button
19 | public static string Rush => GetContentValue();
20 | public static string Engage => GetContentValue();
21 | public static string Organize => GetContentValue();
22 | public static string Up => GetContentValue();
23 | public static string Down => GetContentValue();
24 | public static string Delete => GetContentValue();
25 | public static string AutoConnect => GetContentValue();
26 | public static string AutoSwitch => GetContentValue();
27 | public static string OK => GetContentValue();
28 | public static string Cancel => GetContentValue();
29 |
30 | // Link
31 | public static string ProjectSite => GetContentValue(fallback: Properties.Resources.ProjectSite);
32 | public static string License => Properties.Resources.License;
33 |
34 | // Menu
35 | public static string StartSignIn => GetContentValue();
36 | public static string ShowAvailable => GetContentValue();
37 | public static string Close => GetContentValue();
38 |
39 | // Message
40 | public static string NotWorkable => GetContentValue();
41 | public static string RecordException => GetContentValue();
42 | public static string OpenLocation => GetContentValue();
43 |
44 | #endregion
45 |
46 | private static Dictionary _languageContent;
47 |
48 | private static string GetContentValue([CallerMemberName] string key = null, string fallback = null)
49 | {
50 | return _languageContent.TryGetValue(key, out string value)
51 | ? value
52 | : fallback ?? key;
53 | }
54 |
55 | public static async Task InitializeAsync()
56 | {
57 | var culture = CultureInfo.CurrentUICulture;
58 | var content = await Task.Run(() => RetrieveLanguageContentFromFile(culture)).ConfigureAwait(false);
59 | #if !DEBUG
60 | content ??= RetrieveLanguageContentFromResources(culture);
61 | #endif
62 | content += Environment.NewLine + RetrieveLanguageContentFromResources(new CultureInfo("en")); // fallback
63 |
64 | _languageContent = ParseLanguageContent(content);
65 | }
66 |
67 | #region Base
68 |
69 | private static readonly Regex _languageFilePattern = new("^language[._](?[a-z]{2,3}(|[-_][a-zA-Z]+)(|[-_][a-zA-Z]+))(|[._]txt)$"); // e.g. language.en.txt or language_en
70 | private const char Delimiter = '=';
71 |
72 | private static string RetrieveLanguageContentFromFile(CultureInfo culture)
73 | {
74 | var folderPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
75 | if (string.IsNullOrEmpty(folderPath))
76 | return null;
77 |
78 | var sources = Directory.GetFiles(folderPath)
79 | .Select(x => (match: _languageFilePattern.Match(Path.GetFileName(x)), filePath: x))
80 | .Where(x => x.match.Success)
81 | .ToDictionary(x => x.match.Groups["name"].Value, x => x.filePath);
82 | if (sources.Count == 0)
83 | return null;
84 |
85 | var buffer = culture;
86 | while (true)
87 | {
88 | if (sources.TryGetValue(buffer.Name, out string filePath))
89 | return File.ReadAllText(filePath);
90 |
91 | buffer = buffer.Parent;
92 | if (buffer == CultureInfo.InvariantCulture)
93 | return null;
94 | }
95 | }
96 |
97 | private static string RetrieveLanguageContentFromResources(CultureInfo culture)
98 | {
99 | var sources = Properties.Resources.ResourceManager.GetResourceSet(CultureInfo.InvariantCulture, true, true)
100 | .Cast()
101 | .Select(x => (match: _languageFilePattern.Match(x.Key.ToString()), x.Value))
102 | .Where(x => x.match.Success)
103 | .ToDictionary(x => x.match.Groups["name"].Value.Replace('_', '-'), x => x.Value.ToString());
104 |
105 | var buffer = culture;
106 | while (true)
107 | {
108 | if (sources.TryGetValue(buffer.Name, out string content))
109 | return content;
110 |
111 | buffer = buffer.Parent;
112 | if (buffer == CultureInfo.InvariantCulture)
113 | return null;
114 | }
115 | }
116 |
117 | private static Dictionary ParseLanguageContent(string source)
118 | {
119 | return source.Split(["\r\n", "\n"], StringSplitOptions.RemoveEmptyEntries)
120 | .Select(x => x.Split([Delimiter], 2))
121 | .Select(x => x.Select(y => y.Trim()).Where(y => !string.IsNullOrEmpty(y)).ToArray())
122 | .Where(x => x.Length == 2) // Both key and value are not empty.
123 | .Select(x => (key: x[0], value: x[1]))
124 | .GroupBy(x => x.key)
125 | .ToDictionary(x => x.Key, x => x.FirstOrDefault().value);
126 | }
127 |
128 | #endregion
129 | }
--------------------------------------------------------------------------------
/Source/Wifinian/Models/LocationInfo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.Windows;
4 |
5 | using Wifinian.Helper;
6 |
7 | namespace Wifinian.Models;
8 |
9 | internal class LocationInfo
10 | {
11 | public static bool PromptOpenLocation(Exception ex)
12 | {
13 | if ((ex is UnauthorizedAccessException or { InnerException: UnauthorizedAccessException })
14 | && OsVersion.Is11Build26100OrGreater)
15 | {
16 | if (MessageBox.Show(
17 | LanguageService.OpenLocation,
18 | ProductInfo.Title,
19 | MessageBoxButton.YesNo,
20 | MessageBoxImage.Exclamation,
21 | MessageBoxResult.Yes) is MessageBoxResult.Yes)
22 | {
23 | Process.Start("explorer.exe", "ms-settings:privacy-location");
24 | return true;
25 | }
26 | }
27 | return false;
28 | }
29 | }
--------------------------------------------------------------------------------
/Source/Wifinian/Models/Logger.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.IO;
4 | using System.Text;
5 | using System.Windows;
6 |
7 | namespace Wifinian.Models;
8 |
9 | internal class Logger
10 | {
11 | private const string OperationFileName = "operation.log";
12 | private const string ExceptionFileName = "exception.log";
13 |
14 | private const string HeaderStart = "[Date:";
15 | private static string ComposeHeader() => $"{HeaderStart} {DateTime.Now} Ver: {ProductInfo.Version}]";
16 |
17 | ///
18 | /// Records operation log to AppData.
19 | ///
20 | /// Content
21 | ///
22 | /// A log file of previous dates will be overridden.
23 | ///
24 | public static void RecordOperation(string content)
25 | {
26 | content = ComposeHeader() + Environment.NewLine
27 | + content + Environment.NewLine + Environment.NewLine;
28 |
29 | RecordToAppData(OperationFileName, content);
30 | }
31 |
32 | ///
33 | /// Records exception log to AppData and Desktop.
34 | ///
35 | /// Exception
36 | ///
37 | /// The log file will be appended with new content as long as one day has not yet passed
38 | /// since last write. Otherwise, the log file will be overwritten.
39 | ///
40 | public static void RecordException(Exception exception)
41 | {
42 | if (exception is null)
43 | throw new ArgumentNullException(nameof(exception));
44 |
45 | var content = ComposeHeader() + Environment.NewLine
46 | + exception.ToString() + Environment.NewLine + Environment.NewLine;
47 |
48 | RecordToAppData(ExceptionFileName, content);
49 |
50 | if (MessageBox.Show(
51 | LanguageService.RecordException,
52 | ProductInfo.Title,
53 | MessageBoxButton.YesNo,
54 | MessageBoxImage.Error,
55 | MessageBoxResult.Yes) != MessageBoxResult.Yes)
56 | return;
57 |
58 | RecordToDesktop(ExceptionFileName, content, 10);
59 | }
60 |
61 | #region Helper
62 |
63 | private static void RecordToAppData(string fileName, string content, int capacity = 1)
64 | {
65 | try
66 | {
67 | var appDataFolderPath = AppDataService.EnsureFolderPath();
68 | var appDataFilePath = Path.Combine(appDataFolderPath, fileName);
69 |
70 | UpdateText(appDataFilePath, content, capacity);
71 | }
72 | catch (Exception ex)
73 | {
74 | Trace.WriteLine("Failed to record log to AppData." + Environment.NewLine
75 | + ex);
76 | }
77 | }
78 |
79 | private static void RecordToDesktop(string fileName, string content, int capacity = 1)
80 | {
81 | try
82 | {
83 | var desktopFilePath = Path.Combine(
84 | Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory),
85 | fileName);
86 |
87 | UpdateText(desktopFilePath, content, capacity);
88 | }
89 | catch (Exception ex)
90 | {
91 | Trace.WriteLine("Failed to record log to Desktop." + Environment.NewLine
92 | + ex);
93 | }
94 | }
95 |
96 | private static void SaveText(string filePath, string content)
97 | {
98 | using var sw = new StreamWriter(filePath, false, Encoding.UTF8); // BOM will be emitted.
99 | sw.Write(content);
100 | }
101 |
102 | private static void UpdateText(string filePath, string newContent, int capacity)
103 | {
104 | string oldContent = null;
105 |
106 | if ((1 < capacity) && File.Exists(filePath) && (File.GetLastWriteTime(filePath) > DateTime.Now.AddDays(-1)))
107 | {
108 | using (var sr = new StreamReader(filePath, Encoding.UTF8))
109 | oldContent = sr.ReadToEnd();
110 |
111 | oldContent = TruncateSections(oldContent, HeaderStart, capacity - 1);
112 | }
113 |
114 | SaveText(filePath, oldContent + newContent);
115 | }
116 |
117 | private static string TruncateSections(string source, string sectionHeader, int sectionCount)
118 | {
119 | if (string.IsNullOrEmpty(sectionHeader))
120 | throw new ArgumentNullException(nameof(sectionHeader));
121 | if (sectionCount <= 0)
122 | throw new ArgumentOutOfRangeException(nameof(sectionCount), sectionCount, "The count must be greater than 0.");
123 |
124 | if (string.IsNullOrEmpty(source))
125 | return string.Empty;
126 |
127 | var separator = Environment.NewLine + sectionHeader;
128 | int foundIndex = source.Length - 1;
129 | int startIndex = 0;
130 |
131 | for (int i = sectionCount; i > 0; i--)
132 | {
133 | foundIndex = source.LastIndexOf(separator, foundIndex, StringComparison.Ordinal);
134 | if (foundIndex < 0)
135 | {
136 | if (source.StartsWith(sectionHeader, StringComparison.Ordinal))
137 | {
138 | startIndex = 0;
139 | }
140 | break;
141 | }
142 | startIndex = foundIndex + Environment.NewLine.Length;
143 | }
144 | return source.Substring(startIndex);
145 | }
146 |
147 | #endregion
148 | }
--------------------------------------------------------------------------------
/Source/Wifinian/Models/ProductInfo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Configuration;
3 | using System.Reflection;
4 | using System.Runtime.CompilerServices;
5 |
6 | namespace Wifinian.Models;
7 |
8 | public static class ProductInfo
9 | {
10 | private static readonly Assembly _assembly = Assembly.GetExecutingAssembly();
11 |
12 | public static Version Version { get; } = _assembly.GetName().Version;
13 |
14 | public static string Product { get; } = _assembly.GetAttribute().Product;
15 |
16 | public static string Title { get; } = _assembly.GetAttribute().Title;
17 |
18 | public static string StartupTaskId => GetAppSettings();
19 |
20 | private static TAttribute GetAttribute(this Assembly assembly) where TAttribute : Attribute =>
21 | (TAttribute)Attribute.GetCustomAttribute(assembly, typeof(TAttribute));
22 |
23 | private static string GetAppSettings([CallerMemberName] string key = null) =>
24 | ConfigurationManager.AppSettings[key];
25 | }
--------------------------------------------------------------------------------
/Source/Wifinian/Models/Settings.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.Reactive.Linq;
4 | using System.Windows;
5 | using Reactive.Bindings.Extensions;
6 |
7 | using Wifinian.Common;
8 |
9 | namespace Wifinian.Models;
10 |
11 | ///
12 | /// Persistent settings
13 | ///
14 | public class Settings : DisposableBase
15 | {
16 | public static Settings Current => _current.Value;
17 | private static readonly Lazy _current = new(() => new Settings());
18 |
19 | private Settings()
20 | { }
21 |
22 | #region Settings
23 |
24 | private const int MinInterval = 10;
25 | private const int MaxInterval = 30;
26 |
27 | public int RescanInterval
28 | {
29 | get => _rescanInterval;
30 | set => SetProperty(ref _rescanInterval, value, MinInterval, MaxInterval);
31 | }
32 | private int _rescanInterval = 30; // Default
33 |
34 | public void IncrementRescanInterval() =>
35 | RescanInterval = IncrementLoop(RescanInterval, MinInterval, MaxInterval);
36 |
37 | private const int MinThreshold = 50;
38 | private const int MaxThreshold = 90;
39 |
40 | public int SignalThreshold
41 | {
42 | get => _signalThreshold;
43 | set => SetProperty(ref _signalThreshold, value, MinThreshold, MaxThreshold);
44 | }
45 | private int _signalThreshold = 50; // Default
46 |
47 | public void IncrementSignalThreshold() =>
48 | SignalThreshold = IncrementLoop(SignalThreshold, MinThreshold, MaxThreshold);
49 |
50 | private static int IncrementLoop(int value, int min, int max)
51 | {
52 | var buffer = (value / 10 + 1) * 10;
53 | return (buffer <= max) ? buffer : min;
54 | }
55 |
56 | public bool EngagesPriority
57 | {
58 | get => _engagesPriority;
59 | set => SetProperty(ref _engagesPriority, value);
60 | }
61 | private bool _engagesPriority;
62 |
63 | public bool ShowsAvailable
64 | {
65 | get => _showsAvailable;
66 | set => SetProperty(ref _showsAvailable, value);
67 | }
68 | private bool _showsAvailable;
69 |
70 | public Size MainWindowSize
71 | {
72 | get => _mainWindowSize;
73 | set => SetProperty(ref _mainWindowSize, value);
74 | }
75 | private Size _mainWindowSize;
76 |
77 | #endregion
78 |
79 | internal void Initiate()
80 | {
81 | Load(this);
82 |
83 | this.PropertyChangedAsObservable()
84 | .Throttle(TimeSpan.FromMilliseconds(100))
85 | .Subscribe(_ => Save(this))
86 | .AddTo(this.Subscription);
87 | }
88 |
89 | #region Load/Save
90 |
91 | private const string SettingsFileName = "settings.xml";
92 |
93 | private static void Load(T instance) where T : class
94 | {
95 | try
96 | {
97 | AppDataService.Load(instance, SettingsFileName);
98 | }
99 | catch (Exception ex)
100 | {
101 | Trace.WriteLine("Failed to load settings." + Environment.NewLine
102 | + ex);
103 | }
104 | }
105 |
106 | private static void Save(T instance) where T : class
107 | {
108 | try
109 | {
110 | AppDataService.Save(instance, SettingsFileName);
111 | }
112 | catch (Exception ex)
113 | {
114 | Trace.WriteLine("Failed to save settings." + Environment.NewLine
115 | + ex);
116 | }
117 | }
118 |
119 | #endregion
120 | }
--------------------------------------------------------------------------------
/Source/Wifinian/Models/Wlan/IWlanWorker.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using ManagedNativeWifi;
5 |
6 | namespace Wifinian.Models.Wlan;
7 |
8 | internal interface IWlanWorker : IDisposable
9 | {
10 | bool IsWorkable { get; }
11 |
12 | event EventHandler NetworkRefreshed;
13 | event EventHandler AvailabilityChanged;
14 | event EventHandler InterfaceChanged;
15 | event EventHandler ConnectionChanged;
16 | event EventHandler ProfileChanged;
17 |
18 | Task ScanNetworkAsync(TimeSpan timeout);
19 |
20 | Task> GetProfilesAsync();
21 |
22 | Task SetProfileOptionAsync(ProfileItem profileItem);
23 | Task SetProfilePositionAsync(ProfileItem profileItem, int position);
24 | Task RenameProfileAsync(ProfileItem profileItem, string profileName);
25 | Task DeleteProfileAsync(ProfileItem profileItem);
26 |
27 | Task ConnectNetworkAsync(ProfileItem profileItem, TimeSpan timeout);
28 | Task DisconnectNetworkAsync(ProfileItem profileItem, TimeSpan timeout);
29 | }
--------------------------------------------------------------------------------
/Source/Wifinian/Models/Wlan/NativeWifiProfileItem.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text.RegularExpressions;
3 | using ManagedNativeWifi;
4 |
5 | namespace Wifinian.Models.Wlan;
6 |
7 | public class NativeWifiProfileItem : ProfileItem
8 | {
9 | public ProfileType ProfileType { get; }
10 |
11 | private ProfileDocument _document;
12 |
13 | public BssType BssType => _document.BssType;
14 |
15 | public override string AuthenticationString
16 | {
17 | get
18 | {
19 | if (Authentication == default)
20 | {
21 | var match = Regex.Match(_document.Xml, @"(?.+)");
22 | if (match.Success)
23 | return match.Groups["value"].Value;
24 | }
25 | return base.AuthenticationString;
26 | }
27 | }
28 |
29 | public override bool CanSetOptions =>
30 | (ProfileType != ProfileType.GroupPolicy) && (BssType == BssType.Infrastructure);
31 |
32 | public override bool IsAutoConnectEnabled
33 | {
34 | get => _document.IsAutoConnectEnabled;
35 | set
36 | {
37 | if (_document is null) // The base class's constructor may access before _document is filled.
38 | return;
39 |
40 | SetProperty((get: () => _document.IsAutoConnectEnabled, set: v => _document.IsAutoConnectEnabled = v), value);
41 | }
42 | }
43 |
44 | public override bool IsAutoSwitchEnabled
45 | {
46 | get => _document.IsAutoSwitchEnabled;
47 | set
48 | {
49 | if (_document is null) // The base class's constructor may access before _document is filled.
50 | return;
51 |
52 | SetProperty((get: () => _document.IsAutoSwitchEnabled, set: v => _document.IsAutoSwitchEnabled = v), value);
53 | }
54 | }
55 |
56 | public string Xml => _document.Xml;
57 |
58 | #region Constructor
59 |
60 | public NativeWifiProfileItem(
61 | string name,
62 | Guid interfaceId,
63 | string interfaceDescription,
64 | ProfileType profileType,
65 | ProfileDocument document,
66 | int position,
67 | bool isRadioOn,
68 | bool isConnected,
69 | string protocol,
70 | int signal,
71 | float band,
72 | int channel) : base(
73 | name: name,
74 | interfaceId: interfaceId,
75 | interfaceName: null,
76 | interfaceDescription: interfaceDescription,
77 | authentication: document.Authentication,
78 | encryption: document.Encryption,
79 | isAutoConnectEnabled: false,
80 | isAutoSwitchEnabled: false,
81 | position: position,
82 | isRadioOn: isRadioOn,
83 | isConnected: isConnected,
84 | protocol: protocol,
85 | signal: signal,
86 | band: band,
87 | channel: channel)
88 | {
89 | this.ProfileType = profileType;
90 | this._document = document ?? throw new ArgumentNullException(nameof(document));
91 | }
92 |
93 | #endregion
94 |
95 | public override void Copy(ProfileItem other)
96 | {
97 | base.Copy(other);
98 |
99 | if (other is NativeWifiProfileItem item)
100 | {
101 | this._document = item._document;
102 | }
103 | }
104 | }
--------------------------------------------------------------------------------
/Source/Wifinian/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Resources;
3 | using System.Runtime.CompilerServices;
4 | using System.Runtime.InteropServices;
5 | using System.Windows;
6 |
7 | // General Information about an assembly is controlled through the following
8 | // set of attributes. Change these attribute values to modify the information
9 | // associated with an assembly.
10 | [assembly: AssemblyTitle("Wifinian")]
11 | [assembly: AssemblyDescription("")]
12 | [assembly: AssemblyConfiguration("")]
13 | [assembly: AssemblyCompany("")]
14 | [assembly: AssemblyProduct("Wifinian")]
15 | [assembly: AssemblyCopyright("Copyright © 2015 emoacht")]
16 | [assembly: AssemblyTrademark("")]
17 | [assembly: AssemblyCulture("")]
18 |
19 | // Setting ComVisible to false makes the types in this assembly not visible
20 | // to COM components. If you need to access a type in this assembly from
21 | // COM, set the ComVisible attribute to true on that type.
22 | [assembly: ComVisible(false)]
23 |
24 | //In order to begin building localizable applications, set
25 | //CultureYouAreCodingWith in your .csproj file
26 | //inside a . For example, if you are using US english
27 | //in your source files, set the to en-US. Then uncomment
28 | //the NeutralResourceLanguage attribute below. Update the "en-US" in
29 | //the line below to match the UICulture setting in the project file.
30 |
31 | //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
32 |
33 |
34 | [assembly: ThemeInfo(
35 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
36 | //(used if a resource is not found in the page,
37 | // or application resource dictionaries)
38 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
39 | //(used if a resource is not found in the page,
40 | // app, or any theme specific resource dictionaries)
41 | )]
42 |
43 |
44 | // Version information for an assembly consists of the following four values:
45 | //
46 | // Major Version
47 | // Minor Version
48 | // Build Number
49 | // Revision
50 | //
51 | // You can specify all the values or you can default the Build and Revision Numbers
52 | // by using the '*' as shown below:
53 | // [assembly: AssemblyVersion("1.0.*")]
54 | [assembly: AssemblyVersion("3.7.2.0")]
55 | [assembly: AssemblyFileVersion("3.7.2.0")]
56 | [assembly: Guid("056ef371-bf6b-42c3-82b7-a113cafa5718")]
57 | [assembly: NeutralResourcesLanguage("en-US")]
58 |
59 | // For unit test
60 | [assembly: InternalsVisibleTo("Wifinian.Test")]
61 |
--------------------------------------------------------------------------------
/Source/Wifinian/Properties/Resources.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace Wifinian.Properties {
12 | using System;
13 |
14 |
15 | ///
16 | /// A strongly-typed resource class, for looking up localized strings, etc.
17 | ///
18 | // This class was auto-generated by the StronglyTypedResourceBuilder
19 | // class via a tool like ResGen or Visual Studio.
20 | // To add or remove a member, edit your .ResX file then rerun ResGen
21 | // with the /str option, or rebuild your VS project.
22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
25 | public class Resources {
26 |
27 | private static global::System.Resources.ResourceManager resourceMan;
28 |
29 | private static global::System.Globalization.CultureInfo resourceCulture;
30 |
31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
32 | internal Resources() {
33 | }
34 |
35 | ///
36 | /// Returns the cached ResourceManager instance used by this class.
37 | ///
38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
39 | public static global::System.Resources.ResourceManager ResourceManager {
40 | get {
41 | if (object.ReferenceEquals(resourceMan, null)) {
42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Wifinian.Properties.Resources", typeof(Resources).Assembly);
43 | resourceMan = temp;
44 | }
45 | return resourceMan;
46 | }
47 | }
48 |
49 | ///
50 | /// Overrides the current thread's CurrentUICulture property for all
51 | /// resource lookups using this strongly typed resource class.
52 | ///
53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
54 | public static global::System.Globalization.CultureInfo Culture {
55 | get {
56 | return resourceCulture;
57 | }
58 | set {
59 | resourceCulture = value;
60 | }
61 | }
62 |
63 | ///
64 | /// Looks up a localized string similar to [Button]
65 | ///Rush=Rush
66 | ///Engage=Engage
67 | ///Organize=Organize
68 | ///Up=Up
69 | ///Down=Down
70 | ///Delete=Delete
71 | ///AutoConnect=Auto connect
72 | ///AutoSwitch=Auto switch
73 | ///OK=OK
74 | ///Cancel=Cancel
75 | ///
76 | ///[Link]
77 | ///ProjectSite=https://github.com/emoacht/Wifinian/blob/master/README.md
78 | ///
79 | ///[Menu]
80 | ///StartSignIn=Start on sign in
81 | ///ShowAvailable=Show available networks only
82 | ///Close=Close
83 | ///
84 | ///[Message]
85 | ///NotWorkable=Can not obtain information on Wi-Fi connections.
86 | ///RecordException=An unexpected problem happened. Save exception log on Desktop?
87 | ///.
88 | ///
89 | public static string language_en {
90 | get {
91 | return ResourceManager.GetString("language_en", resourceCulture);
92 | }
93 | }
94 |
95 | ///
96 | /// Looks up a localized string similar to [Link]
97 | ///ProjectSite=https://github.com/emoacht/Wifinian/blob/master/README_ja.md
98 | ///
99 | ///[Menu]
100 | ///StartSignIn=サインイン時に起動する
101 | ///ShowAvailable=利用可能なネットワークのみ表示する
102 | ///Close=閉じる
103 | ///
104 | ///[Message]
105 | ///NotWorkable=Wi-Fi接続の情報を取得できません。
106 | ///RecordException=予想外の問題が起きました。例外のログをデスクトップに残しますか?
107 | ///.
108 | ///
109 | public static string language_ja_JP {
110 | get {
111 | return ResourceManager.GetString("language_ja_JP", resourceCulture);
112 | }
113 | }
114 |
115 | ///
116 | /// Looks up a localized string similar to https://github.com/emoacht/Wifinian/blob/master/LICENSE.txt.
117 | ///
118 | public static string License {
119 | get {
120 | return ResourceManager.GetString("License", resourceCulture);
121 | }
122 | }
123 |
124 | ///
125 | /// Looks up a localized string similar to https://github.com/emoacht/Wifinian.
126 | ///
127 | public static string ProjectSite {
128 | get {
129 | return ResourceManager.GetString("ProjectSite", resourceCulture);
130 | }
131 | }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/Source/Wifinian/Resources/Icons/AppIcon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emoacht/Wifinian/e17e5989de2be66b3731dcf2a7dc88a505bd5e3f/Source/Wifinian/Resources/Icons/AppIcon.ico
--------------------------------------------------------------------------------
/Source/Wifinian/Resources/Icons/DarkTrayIcon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emoacht/Wifinian/e17e5989de2be66b3731dcf2a7dc88a505bd5e3f/Source/Wifinian/Resources/Icons/DarkTrayIcon.ico
--------------------------------------------------------------------------------
/Source/Wifinian/Resources/Icons/LightTrayIcon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emoacht/Wifinian/e17e5989de2be66b3731dcf2a7dc88a505bd5e3f/Source/Wifinian/Resources/Icons/LightTrayIcon.ico
--------------------------------------------------------------------------------
/Source/Wifinian/Resources/language.en.txt:
--------------------------------------------------------------------------------
1 | [Button]
2 | Rush=Rush
3 | Engage=Engage
4 | Organize=Organize
5 | Up=Up
6 | Down=Down
7 | Delete=Delete
8 | AutoConnect=Auto connect
9 | AutoSwitch=Auto switch
10 | OK=OK
11 | Cancel=Cancel
12 |
13 | [Link]
14 | ProjectSite=https://github.com/emoacht/Wifinian/blob/master/README.md
15 |
16 | [Menu]
17 | StartSignIn=Start on sign in
18 | ShowAvailable=Show available networks only
19 | Close=Close
20 |
21 | [Message]
22 | NotWorkable=Can not obtain information on Wi-Fi connections.
23 | RecordException=An unexpected problem occurred. Save exception log on Desktop?
24 | OpenLocation=On Windows 11 24H2 or newer, to use Wi-Fi functionality, this app needs permission to access location information. Open Privacy & security > Location settings?
--------------------------------------------------------------------------------
/Source/Wifinian/Resources/language.ja-JP.txt:
--------------------------------------------------------------------------------
1 | [Link]
2 | ProjectSite=https://github.com/emoacht/Wifinian/blob/master/README_ja.md
3 |
4 | [Menu]
5 | StartSignIn=サインイン時に起動する
6 | ShowAvailable=利用可能なネットワークのみ表示する
7 | Close=閉じる
8 |
9 | [Message]
10 | NotWorkable=Wi-Fi接続の情報を取得できません。
11 | RecordException=予期しない問題が発生しました。例外ログをデスクトップに保存しますか?
12 | OpenLocation=Windows 11 24H2以降では、Wi-Fiの機能を使うために、このアプリは位置情報へのアクセス許可が必要です。プライバシーとセキュリティ > 位置情報の設定を開きますか?
--------------------------------------------------------------------------------
/Source/Wifinian/ViewModels/MenuWindowViewModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Reactive.Bindings;
3 |
4 | using Wifinian.Common;
5 |
6 | namespace Wifinian.ViewModels;
7 |
8 | public class MenuWindowViewModel : DisposableBase
9 | {
10 | private readonly AppController _controller;
11 |
12 | public ReactiveCommand CloseCommand => _controller?.CloseCommand;
13 |
14 | internal MenuWindowViewModel(AppController controller)
15 | {
16 | this._controller = controller ?? throw new ArgumentNullException(nameof(controller));
17 | }
18 |
19 | #region Startup
20 |
21 | public bool CanRegister => _controller.StartupAgent.CanRegister();
22 |
23 | public bool IsRegistered
24 | {
25 | get => _isRegistered ??= _controller.StartupAgent.IsRegistered();
26 | set
27 | {
28 | if (_isRegistered == value)
29 | return;
30 |
31 | if (value)
32 | {
33 | _controller.StartupAgent.Register();
34 | }
35 | else
36 | {
37 | _controller.StartupAgent.Unregister();
38 | }
39 | _isRegistered = value;
40 | OnPropertyChanged();
41 | }
42 | }
43 | private bool? _isRegistered;
44 |
45 | #endregion
46 | }
--------------------------------------------------------------------------------
/Source/Wifinian/ViewModels/ProfileItemViewModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Reactive.Linq;
4 | using Reactive.Bindings;
5 | using Reactive.Bindings.Extensions;
6 |
7 | using Wifinian.Common;
8 | using Wifinian.Models.Wlan;
9 |
10 | namespace Wifinian.ViewModels;
11 |
12 | public class ProfileItemViewModel : DisposableBase
13 | {
14 | private readonly AppController _controller;
15 |
16 | public string Name
17 | {
18 | get => _name;
19 | set
20 | {
21 | if ((_name is not null) && !_controller.IsUsableProfileName(InterfaceId, value))
22 | return;
23 |
24 | SetProperty(ref _name, value);
25 | }
26 | }
27 | private string _name;
28 |
29 | private Guid InterfaceId { get; }
30 | public string InterfaceDescription { get; }
31 | public string Authentication { get; }
32 | public string Encryption { get; }
33 | public bool CanSetOptions { get; }
34 |
35 | public ReactiveProperty IsAutoConnectEnabled { get; }
36 | public ReactiveProperty IsAutoSwitchEnabled { get; }
37 |
38 | public ReadOnlyReactivePropertySlim Position { get; }
39 | public ReadOnlyReactivePropertySlim PositionCount { get; }
40 |
41 | public ReadOnlyReactivePropertySlim IsRadioOn { get; }
42 | public ReadOnlyReactivePropertySlim IsConnected { get; }
43 |
44 | public ReadOnlyReactivePropertySlim Protocol { get; }
45 | public ReadOnlyReactivePropertySlim Signal { get; }
46 | public ReadOnlyReactivePropertySlim Band { get; }
47 | public ReadOnlyReactivePropertySlim Channel { get; }
48 |
49 | public ReadOnlyReactivePropertySlim IsAvailable { get; }
50 |
51 | public ReactiveProperty IsSelected { get; }
52 |
53 | public ReactiveCommand ConnectCommand { get; }
54 | public ReactiveCommand DisconnectCommand { get; }
55 |
56 | internal ProfileItemViewModel(AppController controller, ProfileItem profileItem)
57 | {
58 | this._controller = controller ?? throw new ArgumentNullException(nameof(controller));
59 |
60 | Name = profileItem.Name;
61 | InterfaceId = profileItem.InterfaceId;
62 | InterfaceDescription = profileItem.InterfaceDescription;
63 | Authentication = profileItem.AuthenticationString.Replace("_", "-");
64 | Encryption = profileItem.Encryption.ToString();
65 | CanSetOptions = profileItem.CanSetOptions;
66 |
67 | this.ObserveProperty(x => x.Name, false)
68 | .Subscribe(async x => await _controller.RenameProfileAsync(profileItem, x))
69 | .AddTo(this.Subscription);
70 |
71 | IsAutoSwitchEnabled = profileItem
72 | .ToReactivePropertyAsSynchronized(x => x.IsAutoSwitchEnabled, ReactivePropertyMode.DistinctUntilChanged)
73 | .AddTo(this.Subscription);
74 | IsAutoConnectEnabled = profileItem
75 | .ToReactivePropertyAsSynchronized(x => x.IsAutoConnectEnabled, ReactivePropertyMode.DistinctUntilChanged)
76 | .AddTo(this.Subscription);
77 | IsAutoConnectEnabled
78 | .Where(x => !x)
79 | .Subscribe(_ => IsAutoSwitchEnabled.Value = false)
80 | .AddTo(this.Subscription);
81 |
82 | Observable.Merge(IsAutoConnectEnabled, IsAutoSwitchEnabled)
83 | .Subscribe(async _ => await _controller.ChangeProfileOptionAsync(profileItem))
84 | .AddTo(this.Subscription);
85 |
86 | Position = profileItem
87 | .ObserveProperty(x => x.Position)
88 | .ToReadOnlyReactivePropertySlim()
89 | .AddTo(this.Subscription);
90 |
91 | PositionCount = profileItem
92 | .ObserveProperty(x => x.PositionCount)
93 | .ToReadOnlyReactivePropertySlim()
94 | .AddTo(this.Subscription);
95 |
96 | IsRadioOn = profileItem
97 | .ObserveProperty(x => x.IsRadioOn)
98 | .ToReadOnlyReactivePropertySlim()
99 | .AddTo(this.Subscription);
100 |
101 | IsConnected = profileItem
102 | .ObserveProperty(x => x.IsConnected)
103 | .ToReadOnlyReactivePropertySlim()
104 | .AddTo(this.Subscription);
105 |
106 | Protocol = profileItem
107 | .ObserveProperty(x => x.Protocol)
108 | .ToReadOnlyReactivePropertySlim()
109 | .AddTo(this.Subscription);
110 |
111 | Signal = profileItem
112 | .ObserveProperty(x => x.Signal)
113 | .ToReadOnlyReactivePropertySlim()
114 | .AddTo(this.Subscription);
115 |
116 | Band = profileItem
117 | .ObserveProperty(x => x.Band)
118 | .ToReadOnlyReactivePropertySlim()
119 | .AddTo(this.Subscription);
120 |
121 | Channel = profileItem
122 | .ObserveProperty(x => x.Channel)
123 | .ToReadOnlyReactivePropertySlim()
124 | .AddTo(this.Subscription);
125 |
126 | IsAvailable = Signal
127 | .Select(x => 0 < x)
128 | .ToReadOnlyReactivePropertySlim()
129 | .AddTo(this.Subscription);
130 |
131 | IsSelected = ReactiveProperty.FromObject(profileItem, x => x.IsTarget)
132 | .AddTo(this.Subscription);
133 |
134 | #region Work
135 |
136 | var isNotWorking = _controller.IsWorking
137 | .Inverse()
138 | .StartWith(true) // This is necessary to start combined sequence.
139 | .Publish();
140 |
141 | ConnectCommand = new[] { isNotWorking, IsAvailable, IsConnected.Inverse() }
142 | .CombineLatestValuesAreAllTrue()
143 | .ObserveOnUIDispatcher() // This is for thread access by ReactiveCommand.
144 | .ToReactiveCommand();
145 | ConnectCommand
146 | .Subscribe(async _ => await _controller.ConnectNetworkAsync(profileItem))
147 | .AddTo(this.Subscription);
148 |
149 | DisconnectCommand = new[] { isNotWorking.AsObservable(), IsConnected }
150 | .CombineLatestValuesAreAllTrue()
151 | .ObserveOnUIDispatcher() // This is for thread access by ReactiveCommand.
152 | .ToReactiveCommand();
153 | DisconnectCommand
154 | .Subscribe(async _ => await _controller.DisconnectNetworkAsync(profileItem))
155 | .AddTo(this.Subscription);
156 |
157 | isNotWorking.Connect().AddTo(this.Subscription);
158 |
159 | #endregion
160 | }
161 | }
--------------------------------------------------------------------------------
/Source/Wifinian/Views/Behaviors/FrameworkElementCenterBehavior.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 | using Microsoft.Xaml.Behaviors;
3 |
4 | namespace Wifinian.Views.Behaviors;
5 |
6 | [TypeConstraint(typeof(FrameworkElement))]
7 | public class FrameworkElementCenterBehavior : Behavior
8 | {
9 | #region Property
10 |
11 | ///
12 | /// Target FrameworkElement to be used for centering
13 | ///
14 | public FrameworkElement Target
15 | {
16 | get { return (FrameworkElement)GetValue(TargetProperty); }
17 | set { SetValue(TargetProperty, value); }
18 | }
19 | public static readonly DependencyProperty TargetProperty =
20 | DependencyProperty.Register(
21 | "Target",
22 | typeof(FrameworkElement),
23 | typeof(FrameworkElementCenterBehavior),
24 | new PropertyMetadata(defaultValue: null));
25 |
26 | #endregion
27 |
28 | protected override void OnAttached()
29 | {
30 | base.OnAttached();
31 |
32 | // Default location must be left-top corner.
33 | this.AssociatedObject.HorizontalAlignment = HorizontalAlignment.Left;
34 | this.AssociatedObject.VerticalAlignment = VerticalAlignment.Top;
35 |
36 | this.AssociatedObject.Loaded += OnLoaded;
37 |
38 | // Add handler to any event which is suitable for triggering centering.
39 | this.AssociatedObject.IsEnabledChanged += OnIsEnabledChanged;
40 | }
41 |
42 | protected override void OnDetaching()
43 | {
44 | base.OnDetaching();
45 |
46 | this.AssociatedObject.Loaded -= OnLoaded;
47 |
48 | // Remove added handler.
49 | this.AssociatedObject.IsEnabledChanged -= OnIsEnabledChanged;
50 | }
51 |
52 | private void OnLoaded(object sender, RoutedEventArgs e)
53 | {
54 | Center();
55 | }
56 |
57 | private void OnIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
58 | {
59 | if (!(bool)e.NewValue)
60 | return;
61 |
62 | Center();
63 | }
64 |
65 | ///
66 | /// Makes associated FrameworkElement at the center of target FrameworkElement.
67 | ///
68 | private void Center()
69 | {
70 | if (Target is null)
71 | return;
72 |
73 | var targetLocation = Target.PointToScreen(default);
74 | var currentLocation = this.AssociatedObject.PointToScreen(default);
75 |
76 | var desiredLocationX = targetLocation.X + (Target.ActualWidth - this.AssociatedObject.ActualWidth) / 2D;
77 | var desiredLocationY = targetLocation.Y + (Target.ActualHeight - this.AssociatedObject.ActualHeight) / 2D;
78 |
79 | var desiredMargin = new Thickness(
80 | this.AssociatedObject.Margin.Left + (desiredLocationX - currentLocation.X),
81 | this.AssociatedObject.Margin.Top + (desiredLocationY - currentLocation.Y),
82 | 0,
83 | 0);
84 |
85 | this.AssociatedObject.Margin = desiredMargin;
86 | }
87 | }
--------------------------------------------------------------------------------
/Source/Wifinian/Views/Behaviors/ListBoxHeightBehavior.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reflection;
3 | using System.Windows;
4 | using System.Windows.Controls;
5 | using Microsoft.Xaml.Behaviors;
6 |
7 | namespace Wifinian.Views.Behaviors;
8 |
9 | [TypeConstraint(typeof(ListBox))]
10 | public class ListBoxHeightBehavior : Behavior
11 | {
12 | #region Property
13 |
14 | public double ParentHeight
15 | {
16 | get { return (double)GetValue(ParentHeightProperty); }
17 | set { SetValue(ParentHeightProperty, value); }
18 | }
19 | public static readonly DependencyProperty ParentHeightProperty =
20 | DependencyProperty.Register(
21 | "ParentHeight",
22 | typeof(double),
23 | typeof(ListBoxHeightBehavior),
24 | new PropertyMetadata(
25 | 0D,
26 | (d, e) => ((ListBoxHeightBehavior)d).AdjustHeight((double)e.NewValue)));
27 |
28 | #endregion
29 |
30 | private ScrollViewer _scrollHost;
31 |
32 | protected override void OnAttached()
33 | {
34 | base.OnAttached();
35 |
36 | this.AssociatedObject.Loaded += OnLoaded;
37 | }
38 |
39 | protected override void OnDetaching()
40 | {
41 | base.OnDetaching();
42 |
43 | this.AssociatedObject.Loaded -= OnLoaded;
44 |
45 | if (_scrollHost is not null)
46 | _scrollHost.ScrollChanged -= OnScrollChanged;
47 | }
48 |
49 | private void OnLoaded(object sender, EventArgs e)
50 | {
51 | // Get internal ScrollHost property.
52 | if (!TryGetNonPublicPropertyValue(this.AssociatedObject, "ScrollHost", out ScrollViewer scrollHost))
53 | return;
54 |
55 | _scrollHost = scrollHost;
56 | _scrollHost.ScrollChanged += OnScrollChanged;
57 | AdjustHeight(ParentHeight);
58 | }
59 |
60 | private void OnScrollChanged(object sender, ScrollChangedEventArgs e)
61 | {
62 | AdjustHeight(ParentHeight);
63 | }
64 |
65 | private void AdjustHeight(double parentHeight)
66 | {
67 | if (_scrollHost is null)
68 | return;
69 |
70 | this.AssociatedObject.Height = Math.Min(_scrollHost.ExtentHeight, parentHeight);
71 | }
72 |
73 | private static bool TryGetNonPublicPropertyValue(TInstance instance, string propertyName, out TValue fieldValue)
74 | {
75 | var fieldInfo = typeof(TInstance).GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Instance);
76 | if (fieldInfo?.GetValue(instance) is TValue value)
77 | {
78 | fieldValue = value;
79 | return true;
80 | }
81 | fieldValue = default;
82 | return false;
83 | }
84 | }
--------------------------------------------------------------------------------
/Source/Wifinian/Views/Behaviors/ListBoxSelectedItemBehavior.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using System.Windows;
3 | using System.Windows.Controls;
4 | using Microsoft.Xaml.Behaviors;
5 |
6 | namespace Wifinian.Views.Behaviors;
7 |
8 | [TypeConstraint(typeof(ListBox))]
9 | public class ListBoxSelectedItemBehavior : Behavior
10 | {
11 | #region Property
12 |
13 | public bool IsSelectionSwitched
14 | {
15 | get { return (bool)GetValue(IsSelectionSwitchedProperty); }
16 | set { SetValue(IsSelectionSwitchedProperty, value); }
17 | }
18 | public static readonly DependencyProperty IsSelectionSwitchedProperty =
19 | DependencyProperty.Register(
20 | "IsSelectionSwitched",
21 | typeof(bool),
22 | typeof(ListBoxSelectedItemBehavior),
23 | new PropertyMetadata(false, OnIsSelectionSwitched));
24 |
25 | #endregion
26 |
27 | protected override void OnAttached()
28 | {
29 | base.OnAttached();
30 |
31 | this.AssociatedObject.SelectionChanged += OnSelectionChanged;
32 | }
33 |
34 | protected override void OnDetaching()
35 | {
36 | base.OnDetaching();
37 |
38 | this.AssociatedObject.SelectionChanged -= OnSelectionChanged;
39 | }
40 |
41 | private static void OnIsSelectionSwitched(DependencyObject d, DependencyPropertyChangedEventArgs e)
42 | {
43 | ((ListBoxSelectedItemBehavior)d).AssociatedObject.SelectedIndex = -1; // Not selected
44 | }
45 |
46 | private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
47 | {
48 | var listBox = (ListBox)sender;
49 |
50 | // Keep selected item always single while SelectionMode is Multiple.
51 | if (listBox is { SelectedItems.Count: > 1 })
52 | {
53 | var lastSelectedItem = e.AddedItems.Cast