├── .gitattributes ├── .gitignore ├── LICENSE.md ├── README.md ├── TabViewTear.sln └── TabViewTear ├── .editorconfig ├── Activation ├── ActivationHandler.cs └── DefaultLaunchActivationHandler.cs ├── App.xaml ├── App.xaml.cs ├── Assets ├── LargeTile.scale-200.png ├── SmallTile.scale-200.png ├── SplashScreen.scale-200.png ├── Square150x150Logo.scale-200.png ├── Square44x44Logo.altform-unplated_targetsize-16.png ├── Square44x44Logo.altform-unplated_targetsize-48.png ├── Square44x44Logo.scale-100.png ├── Square44x44Logo.scale-200.png ├── Square44x44Logo.targetsize-16.png ├── Square44x44Logo.targetsize-48.png ├── StoreLogo.scale-100.png ├── StoreLogo.scale-200.png └── Wide310x150Logo.scale-200.png ├── Helpers ├── Json.cs ├── ResourceExtensions.cs ├── SettingsStorageExtensions.cs └── Singleton.cs ├── Models └── DataItem.cs ├── Package.appxmanifest ├── Properties ├── AssemblyInfo.cs └── Default.rd.xml ├── Services ├── ActivationService.cs ├── MessageEventArgs.cs ├── NavigationService.cs ├── ThemeSelectorService.cs ├── ViewLifetimeControl.cs └── WindowManagerService.cs ├── Strings └── en-us │ └── Resources.resw ├── Styles ├── Page.xaml ├── TextBlock.xaml ├── _Colors.xaml ├── _FontSizes.xaml └── _Thickness.xaml ├── TabViewTear.csproj └── Views ├── MainPage.xaml └── MainPage.xaml.cs /.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 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # TabView Tear-Off Sample 2 | 3 | Copyright (c) .NET Foundation and Contributors 4 | 5 | All rights reserved. 6 | 7 | # MIT License (MIT) 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | topic: sample 3 | languages: 4 | - csharp 5 | products: 6 | - windows 7 | - windows-uwp 8 | --- 9 | 10 | TabView Tear-Off Sample 11 | ======================= 12 | 13 | This sample demonstrates how to use the [Windows Community Toolkit](https://github.com/windows-toolkit/WindowsCommunityToolkit/)'s [TabView](https://docs.microsoft.com/en-us/windows/communitytoolkit/controls/tabview 14 | ) control in combination with [Windows Template Studio](https://github.com/microsoft/windowsTemplateStudio)'s [Multiple Views](https://github.com/Microsoft/WindowsTemplateStudio/blob/dev/docs/features/multiple-views.md) support to show how to emulate Microsoft Edge's tear-off tab windowing in your UWP app. It also demonstrates how to place Tabs in the Title Bar of the Application and properly handle Full Screen support. 15 | 16 | Requirements 17 | ------------ 18 | Requires VS 2017 and Windows 10 version 16299 or above. 19 | 20 | Dependencies 21 | ------------ 22 | - [Windows Community Toolkit 5.0](https://github.com/windows-toolkit/WindowsCommunityToolkit/) 23 | - [Json.NET](https://www.newtonsoft.com/json) 24 | 25 | Considerations 26 | -------------- 27 | 1. Each Window runs its own Thread, this has implications on data transfer, Window messaging, and UI Page/Control construction. 28 | 29 | 2. When constructing a new Window, it needs it's own UI shell to be reconstructed. 30 | 31 | 3. This samples assumes the implementor will be using a collection of custom data items bound to the TabView. 32 | 33 | 4. This sample assumes all Windows are managed by the same process and shares the same implementation for each Window. 34 | 35 | Known Issues 36 | ------------ 37 | 1. Dragging a tab to another monitor/position doesn't open the window on the other monitor/position. 38 | 39 | This is a platform limitation for two reasons, A) we can't determine which monitor the user has dropped the item on, and B) we can't request the window to be opened at a specific location. 40 | 41 | This also causes an issue if the origin window is FullScreen as the new window is created on the same monitor and takes focus away from the original window, making it look like FullScreen mode has been exited. Clicking on the Taskbar Icon for the app restores the origin window to FullScreen as expected. 42 | 43 | 2. The right-most tab will disappear when dragging a tab to another window. 44 | 45 | This is a known bug which needs to be resolved in the TabView control, see [Issue #2670](https://github.com/windows-toolkit/WindowsCommunityToolkit/issues/2670). 46 | 47 | 3. Dragging into a FullScreen Window is not supported. 48 | 49 | Currently, the hidden TitleBar of a FullScreen app doesn't appear when performing any drag operation. We could mitigate this in the future by detecting the drag entering our window and toggling the visibility of our TabView (ignoring the visibility of the TitleBar from the system in this scenario). 50 | 51 | About the Sample 52 | ---------------- 53 | 54 | For many years, browsers have allowed users to drag tabs out of their windows to move tabs between monitors. They also let users drag tabs between windows. This scenario is alluring for other document based apps as well. 55 | 56 | This sample demonstrates the main building blocks needed to provide this experience with the new TabView control. There are a few main technical pieces we need to make this scenario work harmoniously: 57 | 58 | 1. Detect dragging a Tab out of the window. 59 | 2. Create a secondary window to display content. 60 | 3. Transfer our tab data to the other window. 61 | 4. Move a tab between two existing windows. 62 | 5. Close a window if the last tab is moved. 63 | 64 | In addition we can provide added app real-estate by properly handling the following: 65 | 1. Placing tabs in the TitleBar. 66 | 2. Handling Full Screen Mode. 67 | 68 | The rest of this article will share how this sample addresses each of these challenges. 69 | 70 | ## Detecting Tab Drag 71 | 72 | Fortunately, this is an easy one as the TabView control [provides a `TabDraggedOutside` event](https://docs.microsoft.com/en-us/windows/communitytoolkit/controls/tabview#events). We can listen to this event to know when the user has requested a tab to leave its window. 73 | 74 | The TabView does this by looking for a drag which had no operation accepted. This means another window or application didn't accept the drag as a valid operation and in our case is an excellent indicator that the user dragged the tab outside the window and wants to 'tear' it off. 75 | 76 | ## Create a Secondary Window 77 | 78 | Once the user has dragged a tab outside of the window, we need to create a Secondary window in order to display the tab. We also need to remove this tab from our original window. 79 | 80 | Fortunately, the Windows Template Studio provides a feature template ("[Multiple views](https://github.com/Microsoft/WindowsTemplateStudio/blob/dev/docs/features/multiple-views.md 81 | )") for setting up and controlling the life-cycle of Secondary Windows. 82 | 83 | In our case, we needed to provide some context for the new window to create itself (the tab's data), so I added a `Context` property to the `ViewLifetimeControl` and modified the `TryShowAsStandaloneAsync` method on `WindowManagerService` to accept this context and add it to the construction of the ViewLifetimeControl. 84 | 85 | This allows us in the `OnNavigatedTo` event of our page to grab this context out of the `Parameter` argument when a secondary window is created. 86 | 87 | Now we can simply create a new window using our same page type and pass it our data (more on this in the next section): 88 | 89 | ```csharp 90 | private async void Items_TabDraggedOutside(object sender, Microsoft.Toolkit.Uwp.UI.Controls.TabDraggedOutsideEventArgs e) 91 | { 92 | if (e.Item is DataItem data && TabItems.Count > 1) // Don't bother creating a new window if we're the last tab, no-op. 93 | { 94 | // Need to serialize item to better provide transfer across window threads. 95 | var lifetimecontrol = await WindowManagerService.Current.TryShowAsStandaloneAsync(data.Title, typeof(MainPage), JsonConvert.SerializeObject(data)); 96 | 97 | // Remove Dragged Tab from this window 98 | TabItems.Remove(data); 99 | } 100 | } 101 | ``` 102 | 103 | ## Transferring Tab Data 104 | 105 | One thing we have to be conscious of when creating a new window is the new window will run on its new thread. While we can technically pass a reference across to the new window to our tab data, this will cause complications from it being originally created on a different thread when we try and access it again. 106 | 107 | To circumvent these issues we use JSON serialization to create a thread neutral package (as a string) to pass data between our windows. This is pretty painless with the help of the [Json.NET](https://www.newtonsoft.com/json) library. 108 | 109 | The sample uses TabView with an `ItemsSource` bound to an `ObservableCollection` of `DataItem` objects. `DataItem` is a custom class we've used to represent our tab data. In this example it simply has `Title` and `Content` properties used to represent the tab header and what is displayed within the tab. However, it could have additional properties. The important thing is that our data is easily serializable into JSON. 110 | 111 | Then we can easily convert between our object and JSON with the following code: 112 | 113 | ```csharp 114 | // Convert an object to a string 115 | var data = new DataItem() { Title = "Test Tab", Content = "Our content." }; 116 | var str = JsonConvert.SerializeObject(data); 117 | 118 | // Convert it back 119 | var datanew = JsonConvert.DeserializeObject(str); 120 | ``` 121 | 122 | However, you could use any other serialization technique here, if desired. JSON is nice as it's still human readable if you need to diagnose any odd problems or also use for saving or interoperate between implementations in other languages or platforms. 123 | 124 | ## Moving Tabs between Windows 125 | 126 | Surprisingly, this is the most difficult task. There are a number of challenges here in this space: 127 | 128 | 1. How to enable drag and drop. 129 | 2. Storing information about the tab. 130 | 3. Creating a new tab when its dropped. 131 | 4. Ensuring the tab is dropped where the user wanted it. 132 | 5. Closing the tab in the originating window. 133 | 134 | ### Drag and Drop 135 | 136 | To enable the Drag and Drop scenario for our TabView we need to enable the following properties and events in XAML: 137 | 138 | ```xml 139 | 148 | ``` 149 | 150 | The first three will let us drag items out of the TabView and let the TabView accept drops. The other events are all the ones we need to register to perform different steps of our drag operation and occur in the following order: 151 | 152 | The `DragItemsStarting` event is where we will save the data and information about the tab needed to move it to a new window. 153 | 154 | The `DragOver` event is needed so the target TabView can accept the drag operation. 155 | 156 | The `Drop` event is used by the target TabView to receive the tab data and construct a new tab in its own window. This is also where we can figure out where the user was trying to drop the tab to put it in the right spot in the target TabView. 157 | 158 | Finally, the `DragItemsCompleted` event is called in our originating window. This is where we do our final clean-up and remove the original tab that was now dragged to the Secondary window. 159 | 160 | ### Tab Info 161 | 162 | Fortunately, we can save our tab just like we did in our other case and add it to our drag properties in our `DragItemsStarting` event: 163 | 164 | ```csharp 165 | private void Items_DragItemsStarting(object sender, DragItemsStartingEventArgs e) 166 | { 167 | // In Initial Window we need to serialize our tab data. 168 | var item = e.Items.FirstOrDefault(); 169 | 170 | if (item is DataItem data) 171 | { 172 | // Add actual data 173 | e.Data.Properties.Add(DataIdentifier, JsonConvert.SerializeObject(data)); 174 | // Add our index so we know where to remove from later (if needed) 175 | e.Data.Properties.Add(DataIndex, Items.IndexFromContainer(Items.ContainerFromItem(data))); 176 | // Add Window Id to know if we're transferring to a different window. 177 | e.Data.Properties.Add(DataWindow, ApplicationView.GetForCurrentView().Id); 178 | } 179 | } 180 | ``` 181 | 182 | The first argument to each of these `Add` calls is just a string which we've made a constant at the top of the class for convenience and consistency. 183 | 184 | We also add information about where the tab was located originally and in which window this tab is from. This will be all the information we need in order to accomplish our task. 185 | 186 | We use properties here rather than storing text as we don't want other applications allowing the tab to be dragged with some text as a target. This should increase the likelihood that an application won't accept the drag so that we know we want to create a new window. 187 | 188 | ### Creating a new Tab on Drop 189 | 190 | In order to accept a drop, we need to first indicate that we want to accept an incoming drop. We do this in the `DragOver` event by accepting the operation: 191 | 192 | ```csharp 193 | private void Items_DragOver(object sender, DragEventArgs e) 194 | { 195 | // Do we have Tab Data? 196 | if (e.DataView.Properties.ContainsKey(DataIdentifier)) 197 | { 198 | // Tell OS that we allow moving item. 199 | e.AcceptedOperation = DataPackageOperation.Move; 200 | } 201 | } 202 | ``` 203 | 204 | We simply check if the thing looks like a tab and then if so, say that we'll accept it. 205 | 206 | This is a requirement for us to get our `Drop` event next. 207 | 208 | The bulk of our `Drop` event in the sample deals with the next section of how we place the tab where the user indicated. The main part we use to get our tab data is at the top: 209 | 210 | ```csharp 211 | if (e.DataView.Properties.TryGetValue(DataIdentifier, out object value) && value is string str) 212 | { 213 | var data = JsonConvert.DeserializeObject(str); 214 | ``` 215 | 216 | Then, we'll then insert the tab data into our `TabItems` collection (more in the next section on that). 217 | 218 | And finally, we'll select the tab that was dropped, see below. However, if we just stopped there the original tab would remain in the first window, so we also need to send a message back to remove it (more on this two sections down). 219 | 220 | ```csharp 221 | Items.SelectedItem = data; // Select new item. 222 | 223 | // Send message to originator to remove the tab. 224 | WindowManagerService.Current.SendMessage((e.DataView.Properties[DataWindow] as int?).Value, CommandClose, e.DataView.Properties[DataIndex]); 225 | ``` 226 | 227 | ### Tab Placement during drag 228 | 229 | Most Drag and Drop examples to another list simply just add the item dropped to the end of the collection. This is in contrast to how drag and drop works within a single list. And the operating system by default shows the nice separation animation to indicate to the user where the item will be dropped in both cases. We'd like our tabs to respect this request by the user. 230 | 231 | To do so, we need to determine where the drop location is in relation to our `TabViewItem` headers. First we get our `TabView` as the sender of the `Drop` event and create a tracker for which index we should drop our tab into our collection: 232 | 233 | ```csharp 234 | // First we need to get the position in the List to drop to 235 | var listview = sender as TabView; 236 | var index = -1; 237 | ``` 238 | 239 | Next we loop through each of our `TabViewItem` objects and check their position in relation to our drop point: 240 | 241 | ```csharp 242 | // Determine which items in the list our pointer is inbetween. 243 | for (int i = 0; i < listview.Items.Count; i++) 244 | { 245 | var item = listview.ContainerFromIndex(i) as TabViewItem; 246 | 247 | if (e.GetPosition(item).X - item.ActualWidth < 0) 248 | { 249 | index = i; 250 | break; 251 | } 252 | } 253 | ``` 254 | 255 | Since our tabs our horizontal, we use the `X` value, but this same method works for vertical lists as well (swapping it out to `Y` and `ActualHeight`). 256 | 257 | We are getting the relative position of the item in relation to the drop point, so we subtract the size of the item to understand where the cursor is in relation to the bounding box. We know that when these values transition to a negative value we're in the vicinity of the mouse cursor's actual location and should use that index to insert our new tab. 258 | 259 | If we go through all our tabs and still have positive values, it means our cursor is at the end of the list. This finally allows us to simply do a check to determine where we need to insert our new tab into our collection: 260 | 261 | ```csharp 262 | if (index < 0) 263 | { 264 | // We didn't find a transition point, so we're at the end of the list 265 | TabItems.Add(data); 266 | } 267 | else if (index < listview.Items.Count) 268 | { 269 | // Otherwise, insert at the provided index. 270 | TabItems.Insert(index, data); 271 | } 272 | ``` 273 | 274 | ### Closing the original Tab 275 | 276 | The last piece of our big puzzle with transferring a tab between windows is removing the tab from the originating window. This isn't a simple task because when the `DragItemsCompleted` event fires we have no information to help us distinguish between a drag within the window and a drag to another window. Both signatures and parameters values in both cases are the same. 277 | 278 | Therefore, we need our receiving window to send a message to tell us to remove our tab. You may have noticed we used a `SendMessage` command on the `WindowManagerService`. This is something that we had to add for this scenario. 279 | 280 | We first created a new `MessageEventArgs` class which could contain information about a message sent between windows. This contained properties such as `FromId` and `ToId` for storing Window identifiers and `Message` and `Data` to aid in routing and information storage. 281 | 282 | **It's important to note**, just like our tab dragging and windowing scenarios, sending messages between windows has the same inherent threading issues. So, we need to be careful about the type of data we send. 283 | 284 | With this structure in place, I added both a `SendMessage` method to the `WindowManagerService` and `ViewLifetimeControl` as well as a `MessageReceived` and `MainMessageReceived` event. The Main Window of the app is a special case, so it needed its own event that the first window could subscribe to, as seen in our `OnNavigatedTo` event where we detect this condition, subscribe to the event, and initialize our starting tabs. 285 | 286 | We now had the infrastructure to send and receive a message between our windows, recalling from before: 287 | 288 | ```csharp 289 | // Send message to originator to remove the tab. 290 | WindowManagerService.Current.SendMessage((e.DataView.Properties[DataWindow] as int?).Value, CommandClose, e.DataView.Properties[DataIndex]); 291 | 292 | // Registered in OnNavigatedTo: 293 | _viewLifetimeControl.MessageReceived += OnViewLifetimeControlMessageReceived; 294 | // Or for Main Window: 295 | WindowManagerService.Current.MainWindowMessageReceived += OnViewLifetimeControlMessageReceived; 296 | ``` 297 | 298 | However, when we receive this message in our drag and drop phase, it's not the right time to act on closing the tab as it has temporarily been removed from the collection already by the drag operation. Therefore we simply store it in a private variable: 299 | 300 | ```csharp 301 | private void OnViewLifetimeControlMessageReceived(object sender, MessageEventArgs e) 302 | { 303 | _lastMsg = e; // Store to complete in DragItemsCompleted. 304 | } 305 | ``` 306 | 307 | Then we can act on it in the case where we detected a move and received a message that it was to another window in our `DragItemsCompleted` event: 308 | 309 | ```csharp 310 | private async void Items_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args) 311 | { 312 | // Remove tab from old window after drag completed, if done when message received, item is not 'back' yet from drag processing. 313 | if (args.DropResult == DataPackageOperation.Move && _lastMsg != null) 314 | { 315 | switch (_lastMsg.Message) 316 | { 317 | case CommandClose: 318 | if (_lastMsg.Data is int value) 319 | { 320 | TabItems.RemoveAt(value); 321 | 322 | if (TabItems.Count == 0) 323 | { 324 | // To cover in the next section, as we want to close the window here. 325 | } 326 | } 327 | 328 | _lastMsg = null; 329 | break; 330 | } 331 | } 332 | } 333 | ``` 334 | 335 | Above, we look at the message we received and if its a 'Close' command (our only one right now) then we remove the tab at the specified index (which we had set originally back in our `DragItemsStarting` event and passed forward). 336 | 337 | Now, we have a functioning drag of a tab across to our other window! 338 | 339 | ## 'Closing' windows no longer needed 340 | 341 | What happens when we drag the last tab out of our window? We normally would expect in this pattern to close the window. 342 | 343 | However, UWP doesn't provide a straight-forward way to tell a window to close, especially our Main Window. 344 | 345 | We can use the following trick though to Consolidate our view to another using the `ApplicationViewSwitcher.SwitchAsync` method. This will let us specify that we want to really be showing a different view instead of our current one. And if that view is already open, then it should just clean-up our old one... 346 | 347 | ```csharp 348 | // No tabs left on main window, 'switch' to window just created to hide the main view 349 | await ApplicationViewSwitcher.SwitchAsync(_lastMsg.FromId, ApplicationView.GetForCurrentView().Id, ApplicationViewSwitchingOptions.ConsolidateViews); 350 | ``` 351 | 352 | With this simple call, we've now cleaned up our empty view and finished our example. 353 | 354 | ## (optional) Tabs in TitleBars 355 | 356 | Many apps with tabs prefer to make use of the TitleBar space of the app to display tabs, like Microsoft Edge. 357 | 358 | We can enable this with a few lines of code and some XAML. First we need to hook into the *CoreApplication's TitleBar* and register some events to detect changes to the `ApplicationView`: 359 | 360 | ```csharp 361 | // https://docs.microsoft.com/en-us/windows/uwp/design/shell/title-bar 362 | var coreTitleBar = CoreApplication.GetCurrentView().TitleBar; 363 | coreTitleBar.ExtendViewIntoTitleBar = true; 364 | 365 | // Register for changes 366 | coreTitleBar.LayoutMetricsChanged += this.CoreTitleBar_LayoutMetricsChanged; 367 | CoreTitleBar_LayoutMetricsChanged(coreTitleBar, null); 368 | 369 | coreTitleBar.IsVisibleChanged += this.CoreTitleBar_IsVisibleChanged; 370 | 371 | // Set XAML element as draggable region. 372 | Window.Current.SetTitleBar(AppTitleBar); 373 | 374 | ``` 375 | 376 | The above code will put the `AppTitleBar` element from our XAML into the TitleBar and have it handle input for the window. It's important that we follow the metrics provided so that the user can still interact and move the window around in places without it entirely being content. 377 | 378 | We can add a placeholder for this Title Bar area in our XAML: 379 | 380 | ```xml 381 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | ``` 392 | 393 | It's hard to put the TabView directly in this specially designated area though, as input behaves differently for the TitleBar in order to allow the user to move the Window based on certain conditions. 394 | 395 | Therefore, we're going to use this placeholder to get all the proper metrics and host the TabView in our main app still. This will give us the flexibility we need to still interact with the TabView as we intend as well as the OS properly detecting the areas we want the user to be able to move the app with. 396 | 397 | We also need to shrink the TabView down a bit to fit in the TitleBar, so we add a Resource to it for that: 398 | 399 | ```xml 400 | 401 | 32 402 | ``` 403 | 404 | We'll then setup the Grid for our app with 2 Rows and 3 Columns: 405 | 406 | ```xml 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | ``` 418 | 419 | The rows represent the area for the TitleBar with the Tabs and our Content. 420 | 421 | The columns represent the padding reserved by the system (for Window control buttons, etc...) and our actual content. We bind our app's columns to the ones we're using with our placeholder in the `AppTitleBar`. 422 | 423 | We then place our TabView in Row 0, Column 1 and our Content in Row 1 with a ColumnSpan of 3. 424 | 425 | The *order* of how we place these things in our XAML is **vitally important** as well. We need to layer things correctly in order to have everything display and be interacted with properly: 426 | 427 | ```xml 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | ``` 438 | 439 | This will enable the TabView to be displayed over the content when in Full Screen mode (see next section) as well as allowing the TitleBar to properly let the user move the Window in areas where the TabView is Transparent. 440 | 441 | Finally, we need to adjust our TabView to not show it's content by default as we'll use a `ContentPresenter` to display the content outside the TabView. This lets us better handle Full Screen mode next as otherwise the content would disappear with our TitleBar. 442 | 443 | We remove the content from the TabView by setting up a blank `DataTemplate`: 444 | 445 | ```xml 446 | 447 | 448 | 449 | ``` 450 | 451 | Then we create `ContentPresenter` at the top of our Grid to host the `SelectedItem`'s content: 452 | 453 | ```xml 454 | 455 | ``` 456 | 457 | We can use a casting in our `x:Bind` expression in order to get our model type's `Content` property. We can even add a `ContentTemplate` to better display our text: 458 | 459 | ```xml 460 | 461 | 462 | 463 | 464 | 465 | ``` 466 | 467 | ## Handling Full Screen 468 | 469 | Did you know that users have a built-in way to make a UWP app Full Screen **Shift+Win+Enter**? We'll add an additional **F11** shortcut as well as a button to help make it clear this is a feature. 470 | 471 | First, we'll add a `IsFullScreen` property to our window to know what mode we're in. Then we'll add some code to detect when it happens automatically and update our value: 472 | 473 | ```csharp 474 | // Listen for Fullscreen Changes from Shift+Win+Enter or our F11 shortcut 475 | ApplicationView.GetForCurrentView().VisibleBoundsChanged += this.MainPage_VisibleBoundsChanged; 476 | ``` 477 | 478 | Then we can use this property to manage the rest of our logic with the VisualStateManager. 479 | 480 | When the app is Full Screen, we want to hide the Full Screen button (the maximize button of the app becomes exit full screen) as well as move our TabView out of the way from being seen and make our content the main focus. We use the VisualStateManager to accomplish these changes to our Grid arrangement: 481 | 482 | ```xml 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | ``` 507 | 508 | We change the background color of our TitleBar area so that when it overlays the content it's not Transparent. It needs to be Transparent when the window is outside of Full Screen mode in order to enable the user to interact with it to move the window around. 509 | 510 | The key piece of code driving our VSM is the StateTrigger: 511 | 512 | ```xml 513 | 514 | 515 | 516 | ``` 517 | 518 | This lets us automatically drive the correct storyboard based on our `IsFullScreen` property we setup before. 519 | 520 | Finally, we add an extra shortcut key to the accelerators of our app in XAML: 521 | 522 | ```xml 523 | 524 | 525 | 529 | 530 | ``` 531 | 532 | In 1803, they added a property to hide tooltips, this is important as otherwise hovering in locations on the app sometimes can show an 'F11' tooltip where we don't want one. 533 | 534 | And then hook up our buttons and listeners: 535 | 536 | ```csharp 537 | private void MainPage_VisibleBoundsChanged(ApplicationView sender, object args) 538 | { 539 | // Update Fullscreen from other modes of adjusting view (keyboard shortcuts) 540 | IsFullScreen = ApplicationView.GetForCurrentView().IsFullScreenMode; 541 | } 542 | 543 | private void AppFullScreenShortcut(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args) 544 | { 545 | // Toggle FullScreen from F11 Keyboard Shortcut 546 | if (!IsFullScreen) 547 | { 548 | IsFullScreen = ApplicationView.GetForCurrentView().TryEnterFullScreenMode(); 549 | } 550 | else 551 | { 552 | ApplicationView.GetForCurrentView().ExitFullScreenMode(); 553 | IsFullScreen = false; 554 | } 555 | } 556 | 557 | private void Button_FullScreen_Click(object sender, RoutedEventArgs e) 558 | { 559 | // Redirect to our shortcut key. 560 | AppFullScreenShortcut(null, null); 561 | } 562 | ``` 563 | -------------------------------------------------------------------------------- /TabViewTear.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28010.2048 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TabViewTear", "TabViewTear\TabViewTear.csproj", "{BAE65131-3FC5-4042-A0F6-958783E3B7CC}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|ARM = Debug|ARM 11 | Debug|x64 = Debug|x64 12 | Debug|x86 = Debug|x86 13 | Release|ARM = Release|ARM 14 | Release|x64 = Release|x64 15 | Release|x86 = Release|x86 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {BAE65131-3FC5-4042-A0F6-958783E3B7CC}.Debug|ARM.ActiveCfg = Debug|ARM 19 | {BAE65131-3FC5-4042-A0F6-958783E3B7CC}.Debug|ARM.Build.0 = Debug|ARM 20 | {BAE65131-3FC5-4042-A0F6-958783E3B7CC}.Debug|ARM.Deploy.0 = Debug|ARM 21 | {BAE65131-3FC5-4042-A0F6-958783E3B7CC}.Debug|x64.ActiveCfg = Debug|x64 22 | {BAE65131-3FC5-4042-A0F6-958783E3B7CC}.Debug|x64.Build.0 = Debug|x64 23 | {BAE65131-3FC5-4042-A0F6-958783E3B7CC}.Debug|x64.Deploy.0 = Debug|x64 24 | {BAE65131-3FC5-4042-A0F6-958783E3B7CC}.Debug|x86.ActiveCfg = Debug|x86 25 | {BAE65131-3FC5-4042-A0F6-958783E3B7CC}.Debug|x86.Build.0 = Debug|x86 26 | {BAE65131-3FC5-4042-A0F6-958783E3B7CC}.Debug|x86.Deploy.0 = Debug|x86 27 | {BAE65131-3FC5-4042-A0F6-958783E3B7CC}.Release|ARM.ActiveCfg = Release|ARM 28 | {BAE65131-3FC5-4042-A0F6-958783E3B7CC}.Release|ARM.Build.0 = Release|ARM 29 | {BAE65131-3FC5-4042-A0F6-958783E3B7CC}.Release|ARM.Deploy.0 = Release|ARM 30 | {BAE65131-3FC5-4042-A0F6-958783E3B7CC}.Release|x64.ActiveCfg = Release|x64 31 | {BAE65131-3FC5-4042-A0F6-958783E3B7CC}.Release|x64.Build.0 = Release|x64 32 | {BAE65131-3FC5-4042-A0F6-958783E3B7CC}.Release|x64.Deploy.0 = Release|x64 33 | {BAE65131-3FC5-4042-A0F6-958783E3B7CC}.Release|x86.ActiveCfg = Release|x86 34 | {BAE65131-3FC5-4042-A0F6-958783E3B7CC}.Release|x86.Build.0 = Release|x86 35 | {BAE65131-3FC5-4042-A0F6-958783E3B7CC}.Release|x86.Deploy.0 = Release|x86 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {1C4966DE-414D-4127-942E-7FCF59DDBDC5} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /TabViewTear/.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | end_of_line = crlf 6 | 7 | [*.{cs,xaml}] 8 | indent_style = space 9 | indent_size = 4 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /TabViewTear/Activation/ActivationHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace TabViewTear.Activation 5 | { 6 | // For more information on application activation see https://github.com/Microsoft/WindowsTemplateStudio/blob/master/docs/activation.md 7 | internal abstract class ActivationHandler 8 | { 9 | public abstract bool CanHandle(object args); 10 | 11 | public abstract Task HandleAsync(object args); 12 | } 13 | 14 | internal abstract class ActivationHandler : ActivationHandler 15 | where T : class 16 | { 17 | protected abstract Task HandleInternalAsync(T args); 18 | 19 | public override async Task HandleAsync(object args) 20 | { 21 | await HandleInternalAsync(args as T); 22 | } 23 | 24 | public override bool CanHandle(object args) 25 | { 26 | return args is T && CanHandleInternal(args as T); 27 | } 28 | 29 | protected virtual bool CanHandleInternal(T args) 30 | { 31 | return true; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /TabViewTear/Activation/DefaultLaunchActivationHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | using TabViewTear.Services; 5 | 6 | using Windows.ApplicationModel.Activation; 7 | 8 | namespace TabViewTear.Activation 9 | { 10 | internal class DefaultLaunchActivationHandler : ActivationHandler 11 | { 12 | private readonly Type _navElement; 13 | 14 | public DefaultLaunchActivationHandler(Type navElement) 15 | { 16 | _navElement = navElement; 17 | } 18 | 19 | protected override async Task HandleInternalAsync(LaunchActivatedEventArgs args) 20 | { 21 | // When the navigation stack isn't restored, navigate to the first page and configure 22 | // the new page by passing required information in the navigation parameter 23 | NavigationService.Navigate(_navElement, args.Arguments); 24 | 25 | await Task.CompletedTask; 26 | } 27 | 28 | protected override bool CanHandleInternal(LaunchActivatedEventArgs args) 29 | { 30 | // None of the ActivationHandlers has handled the app activation 31 | return NavigationService.Frame.Content == null; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /TabViewTear/App.xaml: -------------------------------------------------------------------------------- 1 |  6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /TabViewTear/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using TabViewTear.Services; 4 | 5 | using Windows.ApplicationModel.Activation; 6 | using Windows.UI.Xaml; 7 | 8 | namespace TabViewTear 9 | { 10 | public sealed partial class App : Application 11 | { 12 | private Lazy _activationService; 13 | 14 | private ActivationService ActivationService 15 | { 16 | get { return _activationService.Value; } 17 | } 18 | 19 | public App() 20 | { 21 | InitializeComponent(); 22 | 23 | // Deferred execution until used. Check https://msdn.microsoft.com/library/dd642331(v=vs.110).aspx for further info on Lazy class. 24 | _activationService = new Lazy(CreateActivationService); 25 | } 26 | 27 | protected override async void OnLaunched(LaunchActivatedEventArgs args) 28 | { 29 | if (!args.PrelaunchActivated) 30 | { 31 | await ActivationService.ActivateAsync(args); 32 | } 33 | } 34 | 35 | protected override async void OnActivated(IActivatedEventArgs args) 36 | { 37 | await ActivationService.ActivateAsync(args); 38 | } 39 | 40 | private ActivationService CreateActivationService() 41 | { 42 | return new ActivationService(this, typeof(Views.MainPage)); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /TabViewTear/Assets/LargeTile.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CommunityToolkit/Sample-TabView-TearOff/2438bfe0b7adac20d48fafa70a178c5706f344f5/TabViewTear/Assets/LargeTile.scale-200.png -------------------------------------------------------------------------------- /TabViewTear/Assets/SmallTile.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CommunityToolkit/Sample-TabView-TearOff/2438bfe0b7adac20d48fafa70a178c5706f344f5/TabViewTear/Assets/SmallTile.scale-200.png -------------------------------------------------------------------------------- /TabViewTear/Assets/SplashScreen.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CommunityToolkit/Sample-TabView-TearOff/2438bfe0b7adac20d48fafa70a178c5706f344f5/TabViewTear/Assets/SplashScreen.scale-200.png -------------------------------------------------------------------------------- /TabViewTear/Assets/Square150x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CommunityToolkit/Sample-TabView-TearOff/2438bfe0b7adac20d48fafa70a178c5706f344f5/TabViewTear/Assets/Square150x150Logo.scale-200.png -------------------------------------------------------------------------------- /TabViewTear/Assets/Square44x44Logo.altform-unplated_targetsize-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CommunityToolkit/Sample-TabView-TearOff/2438bfe0b7adac20d48fafa70a178c5706f344f5/TabViewTear/Assets/Square44x44Logo.altform-unplated_targetsize-16.png -------------------------------------------------------------------------------- /TabViewTear/Assets/Square44x44Logo.altform-unplated_targetsize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CommunityToolkit/Sample-TabView-TearOff/2438bfe0b7adac20d48fafa70a178c5706f344f5/TabViewTear/Assets/Square44x44Logo.altform-unplated_targetsize-48.png -------------------------------------------------------------------------------- /TabViewTear/Assets/Square44x44Logo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CommunityToolkit/Sample-TabView-TearOff/2438bfe0b7adac20d48fafa70a178c5706f344f5/TabViewTear/Assets/Square44x44Logo.scale-100.png -------------------------------------------------------------------------------- /TabViewTear/Assets/Square44x44Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CommunityToolkit/Sample-TabView-TearOff/2438bfe0b7adac20d48fafa70a178c5706f344f5/TabViewTear/Assets/Square44x44Logo.scale-200.png -------------------------------------------------------------------------------- /TabViewTear/Assets/Square44x44Logo.targetsize-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CommunityToolkit/Sample-TabView-TearOff/2438bfe0b7adac20d48fafa70a178c5706f344f5/TabViewTear/Assets/Square44x44Logo.targetsize-16.png -------------------------------------------------------------------------------- /TabViewTear/Assets/Square44x44Logo.targetsize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CommunityToolkit/Sample-TabView-TearOff/2438bfe0b7adac20d48fafa70a178c5706f344f5/TabViewTear/Assets/Square44x44Logo.targetsize-48.png -------------------------------------------------------------------------------- /TabViewTear/Assets/StoreLogo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CommunityToolkit/Sample-TabView-TearOff/2438bfe0b7adac20d48fafa70a178c5706f344f5/TabViewTear/Assets/StoreLogo.scale-100.png -------------------------------------------------------------------------------- /TabViewTear/Assets/StoreLogo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CommunityToolkit/Sample-TabView-TearOff/2438bfe0b7adac20d48fafa70a178c5706f344f5/TabViewTear/Assets/StoreLogo.scale-200.png -------------------------------------------------------------------------------- /TabViewTear/Assets/Wide310x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CommunityToolkit/Sample-TabView-TearOff/2438bfe0b7adac20d48fafa70a178c5706f344f5/TabViewTear/Assets/Wide310x150Logo.scale-200.png -------------------------------------------------------------------------------- /TabViewTear/Helpers/Json.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | using Newtonsoft.Json; 5 | 6 | namespace TabViewTear.Helpers 7 | { 8 | public static class Json 9 | { 10 | public static async Task ToObjectAsync(string value) 11 | { 12 | return await Task.Run(() => 13 | { 14 | return JsonConvert.DeserializeObject(value); 15 | }); 16 | } 17 | 18 | public static async Task StringifyAsync(object value) 19 | { 20 | return await Task.Run(() => 21 | { 22 | return JsonConvert.SerializeObject(value); 23 | }); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /TabViewTear/Helpers/ResourceExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | using Windows.ApplicationModel.Resources; 5 | 6 | namespace TabViewTear.Helpers 7 | { 8 | internal static class ResourceExtensions 9 | { 10 | private static ResourceLoader _resLoader = new ResourceLoader(); 11 | 12 | public static string GetLocalized(this string resourceKey) 13 | { 14 | return _resLoader.GetString(resourceKey); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /TabViewTear/Helpers/SettingsStorageExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | 5 | using Windows.Storage; 6 | using Windows.Storage.Streams; 7 | 8 | namespace TabViewTear.Helpers 9 | { 10 | // Use these extension methods to store and retrieve local and roaming app data 11 | // More details regarding storing and retrieving app data at https://docs.microsoft.com/windows/uwp/app-settings/store-and-retrieve-app-data 12 | public static class SettingsStorageExtensions 13 | { 14 | private const string FileExtension = ".json"; 15 | 16 | public static bool IsRoamingStorageAvailable(this ApplicationData appData) 17 | { 18 | return appData.RoamingStorageQuota == 0; 19 | } 20 | 21 | public static async Task SaveAsync(this StorageFolder folder, string name, T content) 22 | { 23 | var file = await folder.CreateFileAsync(GetFileName(name), CreationCollisionOption.ReplaceExisting); 24 | var fileContent = await Json.StringifyAsync(content); 25 | 26 | await FileIO.WriteTextAsync(file, fileContent); 27 | } 28 | 29 | public static async Task ReadAsync(this StorageFolder folder, string name) 30 | { 31 | if (!File.Exists(Path.Combine(folder.Path, GetFileName(name)))) 32 | { 33 | return default(T); 34 | } 35 | 36 | var file = await folder.GetFileAsync($"{name}.json"); 37 | var fileContent = await FileIO.ReadTextAsync(file); 38 | 39 | return await Json.ToObjectAsync(fileContent); 40 | } 41 | 42 | public static async Task SaveAsync(this ApplicationDataContainer settings, string key, T value) 43 | { 44 | settings.SaveString(key, await Json.StringifyAsync(value)); 45 | } 46 | 47 | public static void SaveString(this ApplicationDataContainer settings, string key, string value) 48 | { 49 | settings.Values[key] = value; 50 | } 51 | 52 | public static async Task ReadAsync(this ApplicationDataContainer settings, string key) 53 | { 54 | object obj = null; 55 | 56 | if (settings.Values.TryGetValue(key, out obj)) 57 | { 58 | return await Json.ToObjectAsync((string)obj); 59 | } 60 | 61 | return default(T); 62 | } 63 | 64 | public static async Task SaveFileAsync(this StorageFolder folder, byte[] content, string fileName, CreationCollisionOption options = CreationCollisionOption.ReplaceExisting) 65 | { 66 | if (content == null) 67 | { 68 | throw new ArgumentNullException(nameof(content)); 69 | } 70 | 71 | if (string.IsNullOrEmpty(fileName)) 72 | { 73 | throw new ArgumentException("ExceptionSettingsStorageExtensionsFileNameIsNullOrEmpty".GetLocalized(), nameof(fileName)); 74 | } 75 | 76 | var storageFile = await folder.CreateFileAsync(fileName, options); 77 | await FileIO.WriteBytesAsync(storageFile, content); 78 | return storageFile; 79 | } 80 | 81 | public static async Task ReadFileAsync(this StorageFolder folder, string fileName) 82 | { 83 | var item = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false); 84 | 85 | if ((item != null) && item.IsOfType(StorageItemTypes.File)) 86 | { 87 | var storageFile = await folder.GetFileAsync(fileName); 88 | byte[] content = await storageFile.ReadBytesAsync(); 89 | return content; 90 | } 91 | 92 | return null; 93 | } 94 | 95 | public static async Task ReadBytesAsync(this StorageFile file) 96 | { 97 | if (file != null) 98 | { 99 | using (IRandomAccessStream stream = await file.OpenReadAsync()) 100 | { 101 | using (var reader = new DataReader(stream.GetInputStreamAt(0))) 102 | { 103 | await reader.LoadAsync((uint)stream.Size); 104 | var bytes = new byte[stream.Size]; 105 | reader.ReadBytes(bytes); 106 | return bytes; 107 | } 108 | } 109 | } 110 | 111 | return null; 112 | } 113 | 114 | private static string GetFileName(string name) 115 | { 116 | return string.Concat(name, FileExtension); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /TabViewTear/Helpers/Singleton.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | 4 | namespace TabViewTear.Helpers 5 | { 6 | internal static class Singleton 7 | where T : new() 8 | { 9 | private static ConcurrentDictionary _instances = new ConcurrentDictionary(); 10 | 11 | public static T Instance 12 | { 13 | get 14 | { 15 | return _instances.GetOrAdd(typeof(T), (t) => new T()); 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /TabViewTear/Models/DataItem.cs: -------------------------------------------------------------------------------- 1 | using Windows.UI.Xaml; 2 | 3 | namespace TabViewTear.Models 4 | { 5 | public class DataItem: DependencyObject 6 | { 7 | public string Title 8 | { 9 | get { return (string)GetValue(TitleProperty); } 10 | set { SetValue(TitleProperty, value); } 11 | } 12 | 13 | // Using a DependencyProperty as the backing store for Title. This enables animation, styling, binding, etc... 14 | public static readonly DependencyProperty TitleProperty = 15 | DependencyProperty.Register("Title", typeof(string), typeof(DataItem), new PropertyMetadata(string.Empty)); 16 | 17 | public string Content 18 | { 19 | get { return (string)GetValue(ContentProperty); } 20 | set { SetValue(ContentProperty, value); } 21 | } 22 | 23 | // Using a DependencyProperty as the backing store for Content. This enables animation, styling, binding, etc... 24 | public static readonly DependencyProperty ContentProperty = 25 | DependencyProperty.Register("Content", typeof(string), typeof(DataItem), new PropertyMetadata(string.Empty)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /TabViewTear/Package.appxmanifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | TabViewTear 7 | mhawker 8 | Assets\StoreLogo.png 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 | -------------------------------------------------------------------------------- /TabViewTear/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Runtime.CompilerServices; 4 | using System.Runtime.InteropServices; 5 | 6 | // General Information about an assembly is controlled through the following 7 | // set of attributes. Change these attribute values to modify the information 8 | // associated with an assembly. 9 | [assembly: AssemblyTitle("TabViewTear")] 10 | [assembly: AssemblyDescription("")] 11 | [assembly: AssemblyConfiguration("")] 12 | [assembly: AssemblyCompany("")] 13 | [assembly: AssemblyProduct("TabViewTear")] 14 | [assembly: AssemblyCopyright("Copyright © 2018")] 15 | [assembly: AssemblyTrademark("")] 16 | [assembly: AssemblyCulture("")] 17 | 18 | // Version information for an assembly consists of the following four values: 19 | // 20 | // Major Version 21 | // Minor Version 22 | // Build Number 23 | // Revision 24 | // 25 | // You can specify all the values or you can default the Build and Revision Numbers 26 | // by using the '*' as shown below: 27 | // [assembly: AssemblyVersion("1.0.*")] 28 | [assembly: AssemblyVersion("1.0.0.0")] 29 | [assembly: AssemblyFileVersion("1.0.0.0")] 30 | [assembly: ComVisible(false)] 31 | -------------------------------------------------------------------------------- /TabViewTear/Properties/Default.rd.xml: -------------------------------------------------------------------------------- 1 |  17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /TabViewTear/Services/ActivationService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | using TabViewTear.Activation; 7 | 8 | using Windows.ApplicationModel.Activation; 9 | using Windows.System; 10 | using Windows.UI.Core; 11 | using Windows.UI.Xaml; 12 | using Windows.UI.Xaml.Controls; 13 | using Windows.UI.Xaml.Input; 14 | using Windows.UI.Xaml.Navigation; 15 | 16 | namespace TabViewTear.Services 17 | { 18 | // For more information on application activation see https://github.com/Microsoft/WindowsTemplateStudio/blob/master/docs/activation.md 19 | internal class ActivationService 20 | { 21 | private readonly App _app; 22 | private readonly Lazy _shell; 23 | private readonly Type _defaultNavItem; 24 | 25 | public static readonly KeyboardAccelerator AltLeftKeyboardAccelerator = BuildKeyboardAccelerator(VirtualKey.Left, VirtualKeyModifiers.Menu); 26 | 27 | public static readonly KeyboardAccelerator BackKeyboardAccelerator = BuildKeyboardAccelerator(VirtualKey.GoBack); 28 | 29 | public ActivationService(App app, Type defaultNavItem, Lazy shell = null) 30 | { 31 | _app = app; 32 | _shell = shell; 33 | _defaultNavItem = defaultNavItem; 34 | } 35 | 36 | public async Task ActivateAsync(object activationArgs) 37 | { 38 | if (IsInteractive(activationArgs)) 39 | { 40 | // Initialize things like registering background task before the app is loaded 41 | await InitializeAsync(); 42 | 43 | // Do not repeat app initialization when the Window already has content, 44 | // just ensure that the window is active 45 | if (Window.Current.Content == null) 46 | { 47 | // Create a Frame to act as the navigation context and navigate to the first page 48 | Window.Current.Content = _shell?.Value ?? new Frame(); 49 | NavigationService.NavigationFailed += (sender, e) => 50 | { 51 | throw e.Exception; 52 | }; 53 | NavigationService.Navigated += Frame_Navigated; 54 | if (SystemNavigationManager.GetForCurrentView() != null) 55 | { 56 | SystemNavigationManager.GetForCurrentView().BackRequested += ActivationService_BackRequested; 57 | } 58 | } 59 | } 60 | 61 | var activationHandler = GetActivationHandlers() 62 | .FirstOrDefault(h => h.CanHandle(activationArgs)); 63 | 64 | if (activationHandler != null) 65 | { 66 | await activationHandler.HandleAsync(activationArgs); 67 | } 68 | 69 | if (IsInteractive(activationArgs)) 70 | { 71 | var defaultHandler = new DefaultLaunchActivationHandler(_defaultNavItem); 72 | if (defaultHandler.CanHandle(activationArgs)) 73 | { 74 | await defaultHandler.HandleAsync(activationArgs); 75 | } 76 | 77 | // Ensure the current window is active 78 | Window.Current.Activate(); 79 | 80 | // Tasks after activation 81 | await StartupAsync(); 82 | } 83 | } 84 | 85 | private async Task InitializeAsync() 86 | { 87 | WindowManagerService.Current.Initialize(); 88 | await ThemeSelectorService.InitializeAsync(); 89 | } 90 | 91 | private async Task StartupAsync() 92 | { 93 | await ThemeSelectorService.SetRequestedThemeAsync(); 94 | } 95 | 96 | private IEnumerable GetActivationHandlers() 97 | { 98 | yield break; 99 | } 100 | 101 | private bool IsInteractive(object args) 102 | { 103 | return args is IActivatedEventArgs; 104 | } 105 | 106 | private void Frame_Navigated(object sender, NavigationEventArgs e) 107 | { 108 | SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility = NavigationService.CanGoBack ? 109 | AppViewBackButtonVisibility.Visible : AppViewBackButtonVisibility.Collapsed; 110 | } 111 | 112 | private static KeyboardAccelerator BuildKeyboardAccelerator(VirtualKey key, VirtualKeyModifiers? modifiers = null) 113 | { 114 | var keyboardAccelerator = new KeyboardAccelerator() { Key = key }; 115 | if (modifiers.HasValue) 116 | { 117 | keyboardAccelerator.Modifiers = modifiers.Value; 118 | } 119 | 120 | ToolTipService.SetToolTip(keyboardAccelerator, string.Empty); 121 | keyboardAccelerator.Invoked += OnKeyboardAcceleratorInvoked; 122 | return keyboardAccelerator; 123 | } 124 | 125 | private static void OnKeyboardAcceleratorInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args) 126 | { 127 | var result = NavigationService.GoBack(); 128 | args.Handled = result; 129 | } 130 | 131 | private void ActivationService_BackRequested(object sender, BackRequestedEventArgs e) 132 | { 133 | var result = NavigationService.GoBack(); 134 | e.Handled = result; 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /TabViewTear/Services/MessageEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace TabViewTear.Services 8 | { 9 | /// 10 | /// Information about Message sent between Windows. 11 | /// 12 | public class MessageEventArgs: EventArgs 13 | { 14 | public int FromId { get; private set; } 15 | 16 | public int ToId { get; private set; } 17 | 18 | public string Message { get; private set; } 19 | 20 | /// 21 | /// Extra misc data, should be primitive or thread-safe type. 22 | /// 23 | public object Data { get; private set; } 24 | 25 | public MessageEventArgs(int from, int to, string message, object data = null) 26 | { 27 | FromId = from; 28 | ToId = to; 29 | Message = message; 30 | Data = data; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /TabViewTear/Services/NavigationService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Windows.UI.Xaml; 4 | using Windows.UI.Xaml.Controls; 5 | using Windows.UI.Xaml.Media.Animation; 6 | using Windows.UI.Xaml.Navigation; 7 | 8 | namespace TabViewTear.Services 9 | { 10 | public static class NavigationService 11 | { 12 | public static event NavigatedEventHandler Navigated; 13 | 14 | public static event NavigationFailedEventHandler NavigationFailed; 15 | 16 | private static Frame _frame; 17 | private static object _lastParamUsed; 18 | 19 | public static Frame Frame 20 | { 21 | get 22 | { 23 | if (_frame == null) 24 | { 25 | _frame = Window.Current.Content as Frame; 26 | RegisterFrameEvents(); 27 | } 28 | 29 | return _frame; 30 | } 31 | 32 | set 33 | { 34 | UnregisterFrameEvents(); 35 | _frame = value; 36 | RegisterFrameEvents(); 37 | } 38 | } 39 | 40 | public static bool CanGoBack => Frame.CanGoBack; 41 | 42 | public static bool CanGoForward => Frame.CanGoForward; 43 | 44 | public static bool GoBack() 45 | { 46 | if (CanGoBack) 47 | { 48 | Frame.GoBack(); 49 | return true; 50 | } 51 | 52 | return false; 53 | } 54 | 55 | public static void GoForward() => Frame.GoForward(); 56 | 57 | public static bool Navigate(Type pageType, object parameter = null, NavigationTransitionInfo infoOverride = null) 58 | { 59 | // Don't open the same page multiple times 60 | if (Frame.Content?.GetType() != pageType || (parameter != null && !parameter.Equals(_lastParamUsed))) 61 | { 62 | var navigationResult = Frame.Navigate(pageType, parameter, infoOverride); 63 | if (navigationResult) 64 | { 65 | _lastParamUsed = parameter; 66 | } 67 | 68 | return navigationResult; 69 | } 70 | else 71 | { 72 | return false; 73 | } 74 | } 75 | 76 | public static bool Navigate(object parameter = null, NavigationTransitionInfo infoOverride = null) 77 | where T : Page 78 | => Navigate(typeof(T), parameter, infoOverride); 79 | 80 | private static void RegisterFrameEvents() 81 | { 82 | if (_frame != null) 83 | { 84 | _frame.Navigated += Frame_Navigated; 85 | _frame.NavigationFailed += Frame_NavigationFailed; 86 | } 87 | } 88 | 89 | private static void UnregisterFrameEvents() 90 | { 91 | if (_frame != null) 92 | { 93 | _frame.Navigated -= Frame_Navigated; 94 | _frame.NavigationFailed -= Frame_NavigationFailed; 95 | } 96 | } 97 | 98 | private static void Frame_NavigationFailed(object sender, NavigationFailedEventArgs e) => NavigationFailed?.Invoke(sender, e); 99 | 100 | private static void Frame_Navigated(object sender, NavigationEventArgs e) => Navigated?.Invoke(sender, e); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /TabViewTear/Services/ThemeSelectorService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | using TabViewTear.Helpers; 5 | 6 | using Windows.ApplicationModel.Core; 7 | using Windows.Storage; 8 | using Windows.UI.Core; 9 | using Windows.UI.Xaml; 10 | 11 | namespace TabViewTear.Services 12 | { 13 | public static class ThemeSelectorService 14 | { 15 | private const string SettingsKey = "AppBackgroundRequestedTheme"; 16 | 17 | public static ElementTheme Theme { get; set; } = ElementTheme.Default; 18 | 19 | public static async Task InitializeAsync() 20 | { 21 | Theme = await LoadThemeFromSettingsAsync(); 22 | } 23 | 24 | public static async Task SetThemeAsync(ElementTheme theme) 25 | { 26 | Theme = theme; 27 | 28 | await SetRequestedThemeAsync(); 29 | await SaveThemeInSettingsAsync(Theme); 30 | } 31 | 32 | public static async Task SetRequestedThemeAsync() 33 | { 34 | foreach (var view in CoreApplication.Views) 35 | { 36 | await view.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => 37 | { 38 | if (Window.Current.Content is FrameworkElement frameworkElement) 39 | { 40 | frameworkElement.RequestedTheme = Theme; 41 | } 42 | }); 43 | } 44 | } 45 | 46 | private static async Task LoadThemeFromSettingsAsync() 47 | { 48 | ElementTheme cacheTheme = ElementTheme.Default; 49 | string themeName = await ApplicationData.Current.LocalSettings.ReadAsync(SettingsKey); 50 | 51 | if (!string.IsNullOrEmpty(themeName)) 52 | { 53 | Enum.TryParse(themeName, out cacheTheme); 54 | } 55 | 56 | return cacheTheme; 57 | } 58 | 59 | private static async Task SaveThemeInSettingsAsync(ElementTheme theme) 60 | { 61 | await ApplicationData.Current.LocalSettings.SaveAsync(SettingsKey, theme.ToString()); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /TabViewTear/Services/ViewLifetimeControl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using TabViewTear.Helpers; 4 | 5 | using Windows.UI.Core; 6 | using Windows.UI.ViewManagement; 7 | 8 | namespace TabViewTear.Services 9 | { 10 | // A custom event that fires whenever the secondary view is ready to be closed. You should 11 | // clean up any state (including deregistering for events) then close the window in this handler 12 | public delegate void ViewReleasedHandler(object sender, EventArgs e); 13 | 14 | // Whenever the main view is about to interact with the secondary view, it should call 15 | // StartViewInUse on this object. When finished interacting, it should call StopViewInUse. 16 | public sealed class ViewLifetimeControl 17 | { 18 | // Window for this particular view. Used to register and unregister for events 19 | private CoreWindow _window; 20 | private int _refCount = 0; 21 | private bool _released = false; 22 | 23 | private event ViewReleasedHandler InternalReleased; 24 | 25 | // Necessary to communicate with the window 26 | public CoreDispatcher Dispatcher { get; private set; } 27 | 28 | // This id is used in all of the ApplicationViewSwitcher and ProjectionManager APIs 29 | public int Id { get; private set; } 30 | 31 | // Initial title for the window 32 | public string Title { get; set; } 33 | 34 | // Optional context to provide from window opener 35 | public string Context { get; set; } 36 | 37 | public event EventHandler MessageReceived; 38 | 39 | public event ViewReleasedHandler Released 40 | { 41 | add 42 | { 43 | bool releasedCopy = false; 44 | lock (this) 45 | { 46 | releasedCopy = _released; 47 | if (!_released) 48 | { 49 | InternalReleased += value; 50 | } 51 | } 52 | 53 | if (releasedCopy) 54 | { 55 | throw new InvalidOperationException("ExceptionViewLifeTimeControlViewDisposal".GetLocalized()); 56 | } 57 | } 58 | 59 | remove 60 | { 61 | lock (this) 62 | { 63 | InternalReleased -= value; 64 | } 65 | } 66 | } 67 | 68 | private ViewLifetimeControl(CoreWindow newWindow) 69 | { 70 | Dispatcher = newWindow.Dispatcher; 71 | _window = newWindow; 72 | Id = ApplicationView.GetApplicationViewIdForWindow(_window); 73 | RegisterForEvents(); 74 | } 75 | 76 | public static ViewLifetimeControl CreateForCurrentView() 77 | { 78 | return new ViewLifetimeControl(CoreWindow.GetForCurrentThread()); 79 | } 80 | 81 | public void SendMessage(string message, int fromid, object data = null) 82 | { 83 | MessageReceived?.Invoke(this, new MessageEventArgs(fromid, Id, message, data)); 84 | } 85 | 86 | // Signals that the view is being interacted with by another view, 87 | // so it shouldn't be closed even if it becomes "consolidated" 88 | public int StartViewInUse() 89 | { 90 | bool releasedCopy = false; 91 | int refCountCopy = 0; 92 | 93 | lock (this) 94 | { 95 | releasedCopy = _released; 96 | if (!_released) 97 | { 98 | refCountCopy = ++_refCount; 99 | } 100 | } 101 | 102 | if (releasedCopy) 103 | { 104 | throw new InvalidOperationException("ExceptionViewLifeTimeControlViewDisposal".GetLocalized()); 105 | } 106 | 107 | return refCountCopy; 108 | } 109 | 110 | // Should come after any call to StartViewInUse 111 | // Signals that the another view has finished interacting with the view tracked by this object 112 | public int StopViewInUse() 113 | { 114 | int refCountCopy = 0; 115 | bool releasedCopy = false; 116 | 117 | lock (this) 118 | { 119 | releasedCopy = _released; 120 | if (!_released) 121 | { 122 | refCountCopy = --_refCount; 123 | if (refCountCopy == 0) 124 | { 125 | var task = Dispatcher.RunAsync(CoreDispatcherPriority.Low, FinalizeRelease); 126 | } 127 | } 128 | } 129 | 130 | if (releasedCopy) 131 | { 132 | throw new InvalidOperationException("ExceptionViewLifeTimeControlViewDisposal".GetLocalized()); 133 | } 134 | 135 | return refCountCopy; 136 | } 137 | 138 | private void RegisterForEvents() 139 | { 140 | ApplicationView.GetForCurrentView().Consolidated += ViewConsolidated; 141 | } 142 | 143 | private void UnregisterForEvents() 144 | { 145 | ApplicationView.GetForCurrentView().Consolidated -= ViewConsolidated; 146 | } 147 | 148 | private void ViewConsolidated(ApplicationView sender, ApplicationViewConsolidatedEventArgs e) 149 | { 150 | StopViewInUse(); 151 | } 152 | 153 | private void FinalizeRelease() 154 | { 155 | bool justReleased = false; 156 | lock (this) 157 | { 158 | if (_refCount == 0) 159 | { 160 | justReleased = true; 161 | _released = true; 162 | } 163 | } 164 | 165 | if (justReleased) 166 | { 167 | UnregisterForEvents(); 168 | InternalReleased(this, null); 169 | } 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /TabViewTear/Services/WindowManagerService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.ObjectModel; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | using Windows.ApplicationModel.Core; 7 | using Windows.UI.Core; 8 | using Windows.UI.ViewManagement; 9 | using Windows.UI.Xaml; 10 | using Windows.UI.Xaml.Controls; 11 | 12 | namespace TabViewTear.Services 13 | { 14 | public delegate void ViewClosedHandler(ViewLifetimeControl viewControl, EventArgs e); 15 | 16 | // For instructions on testing this service see https://github.com/Microsoft/WindowsTemplateStudio/blob/master/docs/features/multiple-views.md 17 | // More details about showing multiple views at https://docs.microsoft.com/windows/uwp/design/layout/show-multiple-views 18 | public class WindowManagerService 19 | { 20 | private static WindowManagerService _current; 21 | 22 | public static WindowManagerService Current => _current ?? (_current = new WindowManagerService()); 23 | 24 | // Contains all the opened secondary views. 25 | public ObservableCollection SecondaryViews { get; } = new ObservableCollection(); 26 | 27 | public int MainViewId { get; private set; } 28 | 29 | public CoreDispatcher MainDispatcher { get; private set; } 30 | 31 | public event EventHandler MainWindowMessageReceived; 32 | 33 | public void Initialize() 34 | { 35 | MainViewId = ApplicationView.GetForCurrentView().Id; 36 | MainDispatcher = Window.Current.Dispatcher; 37 | } 38 | 39 | // Displays a view as a standalone 40 | // You can use the resulting ViewLifeTileControl to interact with the new window. 41 | public async Task TryShowAsStandaloneAsync(string windowTitle, Type pageType, string dataContext = null) 42 | { 43 | ViewLifetimeControl viewControl = await CreateViewLifetimeControlAsync(windowTitle, pageType, dataContext); 44 | SecondaryViews.Add(viewControl); 45 | viewControl.StartViewInUse(); 46 | var viewShown = await ApplicationViewSwitcher.TryShowAsStandaloneAsync(viewControl.Id, ViewSizePreference.Default, ApplicationView.GetForCurrentView().Id, ViewSizePreference.Default); 47 | viewControl.StopViewInUse(); 48 | return viewControl; 49 | } 50 | 51 | // Displays a view in the specified view mode 52 | public async Task TryShowAsViewModeAsync(string windowTitle, Type pageType, ApplicationViewMode viewMode = ApplicationViewMode.Default) 53 | { 54 | ViewLifetimeControl viewControl = await CreateViewLifetimeControlAsync(windowTitle, pageType); 55 | SecondaryViews.Add(viewControl); 56 | viewControl.StartViewInUse(); 57 | var viewShown = await ApplicationViewSwitcher.TryShowAsViewModeAsync(viewControl.Id, viewMode); 58 | viewControl.StopViewInUse(); 59 | return viewControl; 60 | } 61 | 62 | private async Task CreateViewLifetimeControlAsync(string windowTitle, Type pageType, string dataContext = null) 63 | { 64 | ViewLifetimeControl viewControl = null; 65 | 66 | await CoreApplication.CreateNewView().Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => 67 | { 68 | viewControl = ViewLifetimeControl.CreateForCurrentView(); 69 | viewControl.Title = windowTitle; 70 | viewControl.Context = dataContext; 71 | viewControl.StartViewInUse(); 72 | var frame = new Frame(); 73 | frame.RequestedTheme = ThemeSelectorService.Theme; 74 | frame.Navigate(pageType, viewControl); 75 | Window.Current.Content = frame; 76 | Window.Current.Activate(); 77 | ApplicationView.GetForCurrentView().Title = viewControl.Title; 78 | }); 79 | 80 | return viewControl; 81 | } 82 | 83 | public bool IsWindowOpen(string windowTitle) => SecondaryViews.Any(v => v.Title == windowTitle); 84 | 85 | public ViewLifetimeControl GetWindowById(int id) => SecondaryViews.FirstOrDefault(v => v.Id == id); 86 | 87 | public void SendMessage(int toid, string message, object data = null) 88 | { 89 | if (toid == MainViewId) 90 | { 91 | // Special case for main window 92 | MainWindowMessageReceived?.Invoke(this, new MessageEventArgs(ApplicationView.GetForCurrentView().Id, toid, message, data)); 93 | } 94 | else 95 | { 96 | // Any secondary window 97 | GetWindowById(toid)?.SendMessage(message, ApplicationView.GetForCurrentView().Id, data); 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /TabViewTear/Strings/en-us/Resources.resw: -------------------------------------------------------------------------------- 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 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | TabViewTear 122 | Application display name 123 | 124 | 125 | TabViewTear 126 | Application description 127 | 128 | 129 | File name is null or empty. Specify a valid file name 130 | File name is null or empty to save file in settings storage extensions 131 | 132 | 133 | This view is being disposed. 134 | View disposed 135 | 136 | -------------------------------------------------------------------------------- /TabViewTear/Styles/Page.xaml: -------------------------------------------------------------------------------- 1 |  4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /TabViewTear/Styles/TextBlock.xaml: -------------------------------------------------------------------------------- 1 |  4 | 5 | 6 | 14 | 15 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /TabViewTear/Styles/_Colors.xaml: -------------------------------------------------------------------------------- 1 |  4 | 5 | 6 | -------------------------------------------------------------------------------- /TabViewTear/Styles/_FontSizes.xaml: -------------------------------------------------------------------------------- 1 |  4 | 5 | 28 6 | 16 7 | 8 | 9 | -------------------------------------------------------------------------------- /TabViewTear/Styles/_Thickness.xaml: -------------------------------------------------------------------------------- 1 |  4 | 5 | 6 | 0,0,12,7 7 | 0, 20, 0, 48 8 | 9 | 10 | 12,0,12,0 11 | 12,12,12,12 12 | 13 | 14 | 5, 5, 5, 5 15 | 0, 8, 0, 0 16 | 17 | -------------------------------------------------------------------------------- /TabViewTear/TabViewTear.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | x86 7 | {BAE65131-3FC5-4042-A0F6-958783E3B7CC} 8 | AppContainerExe 9 | Properties 10 | TabViewTear 11 | TabViewTear 12 | en-US 13 | UAP 14 | 10.0.17134.0 15 | 10.0.16299.0 16 | 14 17 | 512 18 | {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 19 | true 20 | TabViewTear_TemporaryKey.pfx 21 | 22 | 23 | true 24 | bin\x86\Debug\ 25 | DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP 26 | NU1603;2008; 27 | full 28 | x86 29 | false 30 | prompt 31 | true 32 | 33 | 34 | bin\x86\Release\ 35 | TRACE;NETFX_CORE;WINDOWS_UWP 36 | true 37 | NU1603;2008; 38 | pdbonly 39 | x86 40 | false 41 | prompt 42 | true 43 | true 44 | 45 | 46 | true 47 | bin\ARM\Debug\ 48 | DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP 49 | NU1603;2008; 50 | full 51 | ARM 52 | false 53 | prompt 54 | true 55 | 56 | 57 | bin\ARM\Release\ 58 | TRACE;NETFX_CORE;WINDOWS_UWP 59 | true 60 | NU1603;2008; 61 | pdbonly 62 | ARM 63 | false 64 | prompt 65 | true 66 | true 67 | 68 | 69 | true 70 | bin\x64\Debug\ 71 | DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP 72 | NU1603;2008; 73 | full 74 | x64 75 | false 76 | prompt 77 | true 78 | 79 | 80 | bin\x64\Release\ 81 | TRACE;NETFX_CORE;WINDOWS_UWP 82 | true 83 | NU1603;2008; 84 | pdbonly 85 | x64 86 | false 87 | prompt 88 | true 89 | true 90 | 91 | 92 | PackageReference 93 | 94 | 95 | 96 | 6.1.7 97 | 98 | 99 | 5.0.0 100 | 101 | 102 | 5.0.0 103 | 104 | 105 | 2.0.0 106 | 107 | 108 | 109 | 11.0.2 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | MainPage.xaml 129 | 130 | 131 | 132 | 133 | App.xaml 134 | 135 | 136 | 137 | 138 | 139 | MSBuild:Compile 140 | Designer 141 | 142 | 143 | Designer 144 | MSBuild:Compile 145 | 146 | 147 | Designer 148 | MSBuild:Compile 149 | 150 | 151 | Designer 152 | MSBuild:Compile 153 | 154 | 155 | Designer 156 | MSBuild:Compile 157 | 158 | 159 | MSBuild:Compile 160 | Designer 161 | 162 | 163 | 164 | 165 | Designer 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | MSBuild:Compile 196 | Designer 197 | 198 | 199 | 200 | 14.0 201 | 202 | 203 | 210 | -------------------------------------------------------------------------------- /TabViewTear/Views/MainPage.xaml: -------------------------------------------------------------------------------- 1 |  13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 102 | 103 | 32 104 | 90 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /TabViewTear/Views/MainPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Toolkit.Uwp.UI.Controls; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.Collections.ObjectModel; 5 | using System.ComponentModel; 6 | using System.Linq; 7 | using System.Runtime.CompilerServices; 8 | using TabViewTear.Models; 9 | using TabViewTear.Services; 10 | using Windows.ApplicationModel.Core; 11 | using Windows.ApplicationModel.DataTransfer; 12 | using Windows.UI.Core; 13 | using Windows.UI.ViewManagement; 14 | using Windows.UI.Xaml; 15 | using Windows.UI.Xaml.Controls; 16 | using Windows.UI.Xaml.Input; 17 | using Windows.UI.Xaml.Navigation; 18 | 19 | namespace TabViewTear.Views 20 | { 21 | public sealed partial class MainPage : Page, INotifyPropertyChanged 22 | { 23 | private const string DataIdentifier = "TabData"; 24 | private const string DataIndex = "TabIndex"; 25 | private const string DataWindow = "TabWindow"; 26 | private const string CommandClose = "Close"; 27 | 28 | ObservableCollection TabItems = new ObservableCollection(); 29 | 30 | public bool IsFullScreen 31 | { 32 | get { return (bool)GetValue(IsFullScreenProperty); } 33 | set { SetValue(IsFullScreenProperty, value); } 34 | } 35 | 36 | // Using a DependencyProperty as the backing store for IsFullScreen. This enables animation, styling, binding, etc... 37 | public static readonly DependencyProperty IsFullScreenProperty = 38 | DependencyProperty.Register(nameof(IsFullScreen), typeof(bool), typeof(MainPage), new PropertyMetadata(false)); 39 | 40 | public MainPage() 41 | { 42 | InitializeComponent(); 43 | 44 | // Hide default title bar. 45 | // https://docs.microsoft.com/en-us/windows/uwp/design/shell/title-bar 46 | var coreTitleBar = CoreApplication.GetCurrentView().TitleBar; 47 | coreTitleBar.ExtendViewIntoTitleBar = true; 48 | 49 | // Register for changes 50 | coreTitleBar.LayoutMetricsChanged += this.CoreTitleBar_LayoutMetricsChanged; 51 | CoreTitleBar_LayoutMetricsChanged(coreTitleBar, null); 52 | 53 | coreTitleBar.IsVisibleChanged += this.CoreTitleBar_IsVisibleChanged; 54 | 55 | // Set XAML element as draggable region. 56 | Window.Current.SetTitleBar(AppTitleBar); 57 | 58 | // Listen for Fullscreen Changes from Shift+Win+Enter or our F11 shortcut 59 | ApplicationView.GetForCurrentView().VisibleBoundsChanged += this.MainPage_VisibleBoundsChanged; 60 | } 61 | 62 | public event PropertyChangedEventHandler PropertyChanged; 63 | 64 | private ViewLifetimeControl _viewLifetimeControl; 65 | 66 | private MessageEventArgs _lastMsg; 67 | 68 | #region Handle Window Lifetime 69 | protected override void OnNavigatedTo(NavigationEventArgs e) 70 | { 71 | base.OnNavigatedTo(e); 72 | 73 | _viewLifetimeControl = e.Parameter as ViewLifetimeControl; 74 | if (_viewLifetimeControl != null) 75 | { 76 | _viewLifetimeControl.StartViewInUse(); 77 | // Register for window close 78 | _viewLifetimeControl.Released += OnViewLifetimeControlReleased; 79 | _viewLifetimeControl.MessageReceived += OnViewLifetimeControlMessageReceived; 80 | // Deserialize passed in item to display in this window 81 | TabItems.Add(JsonConvert.DeserializeObject(_viewLifetimeControl.Context.ToString())); 82 | _viewLifetimeControl.Context = null; 83 | _viewLifetimeControl.StopViewInUse(); 84 | } 85 | else 86 | { 87 | // Main Window Start 88 | InitializeTestData(); 89 | 90 | WindowManagerService.Current.MainWindowMessageReceived += OnViewLifetimeControlMessageReceived; 91 | } 92 | } 93 | 94 | private async void OnViewLifetimeControlReleased(object sender, EventArgs e) 95 | { 96 | _viewLifetimeControl.Released -= OnViewLifetimeControlReleased; 97 | await WindowManagerService.Current.MainDispatcher.RunAsync(CoreDispatcherPriority.Normal, () => 98 | { 99 | WindowManagerService.Current.SecondaryViews.Remove(_viewLifetimeControl); 100 | }); 101 | } 102 | #endregion 103 | 104 | #region Handle Dragging Tab to Create Window 105 | private async void Items_TabDraggedOutside(object sender, Microsoft.Toolkit.Uwp.UI.Controls.TabDraggedOutsideEventArgs e) 106 | { 107 | if (e.Item is DataItem data && TabItems.Count > 1) // Don't bother creating a new window if we're the last tab, no-op. 108 | { 109 | // Need to serialize item to better provide transfer across window threads. 110 | var lifetimecontrol = await WindowManagerService.Current.TryShowAsStandaloneAsync(data.Title, typeof(MainPage), JsonConvert.SerializeObject(data)); 111 | 112 | // Remove Dragged Tab from this window 113 | TabItems.Remove(data); 114 | } 115 | } 116 | #endregion 117 | 118 | #region Handle Tab Change Updating Window Title 119 | private void Items_SelectionChanged(object sender, SelectionChangedEventArgs e) 120 | { 121 | // Update window title with current item 122 | var first = e.AddedItems.FirstOrDefault(); 123 | if (first is DataItem data) 124 | { 125 | ApplicationView.GetForCurrentView().Title = data.Title; 126 | } 127 | } 128 | #endregion 129 | 130 | #region Handle Dragging Tabs between windows 131 | private void Items_DragItemsStarting(object sender, DragItemsStartingEventArgs e) 132 | { 133 | // In Initial Window we need to serialize our tab data. 134 | var item = e.Items.FirstOrDefault(); 135 | 136 | if (item is DataItem data) 137 | { 138 | // Add actual data 139 | e.Data.Properties.Add(DataIdentifier, JsonConvert.SerializeObject(data)); 140 | // Add our index so we know where to remove from later (if needed) 141 | e.Data.Properties.Add(DataIndex, Items.IndexFromContainer(Items.ContainerFromItem(data))); 142 | // Add Window Id to know if we're transferring to a different window. 143 | e.Data.Properties.Add(DataWindow, ApplicationView.GetForCurrentView().Id); 144 | } 145 | } 146 | 147 | private void Items_DragOver(object sender, DragEventArgs e) 148 | { 149 | // Called before we drop to see if we will accept a drop. 150 | 151 | // Do we have Tab Data? 152 | if (e.DataView.Properties.ContainsKey(DataIdentifier)) 153 | { 154 | // Tell OS that we allow moving item. 155 | e.AcceptedOperation = DataPackageOperation.Move; 156 | } 157 | } 158 | 159 | private void Items_Drop(object sender, DragEventArgs e) 160 | { 161 | // Called when we actually get the drop, let's get the data and add our tab. 162 | if (e.DataView.Properties.TryGetValue(DataIdentifier, out object value) && value is string str) 163 | { 164 | var data = JsonConvert.DeserializeObject(str); 165 | 166 | if (data != null) 167 | { 168 | // First we need to get the position in the List to drop to 169 | var listview = sender as TabView; 170 | var index = -1; 171 | 172 | // Determine which items in the list our pointer is inbetween. 173 | for (int i = 0; i < listview.Items.Count; i++) 174 | { 175 | var item = listview.ContainerFromIndex(i) as TabViewItem; 176 | 177 | if (e.GetPosition(item).X - item.ActualWidth < 0) 178 | { 179 | index = i; 180 | break; 181 | } 182 | } 183 | 184 | if (index < 0) 185 | { 186 | // We didn't find a transition point, so we're at the end of the list 187 | TabItems.Add(data); 188 | } 189 | else if (index < listview.Items.Count) 190 | { 191 | // Otherwise, insert at the provided index. 192 | TabItems.Insert(index, data); 193 | } 194 | 195 | Items.SelectedItem = data; // Select new item. 196 | 197 | // Send message to originator to remove the tab. 198 | WindowManagerService.Current.SendMessage((e.DataView.Properties[DataWindow] as int?).Value, CommandClose, e.DataView.Properties[DataIndex]); 199 | } 200 | } 201 | } 202 | 203 | private void OnViewLifetimeControlMessageReceived(object sender, MessageEventArgs e) 204 | { 205 | _lastMsg = e; // Store to complete in DragItemsCompleted. 206 | } 207 | 208 | private async void Items_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args) 209 | { 210 | // Remove tab from old window after drag completed, if done when message received, item is not 'back' yet from drag processing. 211 | if (args.DropResult == DataPackageOperation.Move && _lastMsg != null) 212 | { 213 | switch (_lastMsg.Message) 214 | { 215 | case CommandClose: 216 | if (_lastMsg.Data is int value) 217 | { 218 | TabItems.RemoveAt(value); 219 | 220 | if (TabItems.Count == 0) 221 | { 222 | // No tabs left on main window, 'switch' to window just created to hide the main view 223 | await ApplicationViewSwitcher.SwitchAsync(_lastMsg.FromId, ApplicationView.GetForCurrentView().Id, ApplicationViewSwitchingOptions.ConsolidateViews); 224 | } 225 | } 226 | 227 | _lastMsg = null; 228 | break; 229 | } 230 | } 231 | } 232 | #endregion 233 | 234 | #region Handle App TitleBar 235 | private void CoreTitleBar_IsVisibleChanged(CoreApplicationViewTitleBar sender, object args) 236 | { 237 | // Adjust our content based on the Titlebar's visibility 238 | // This is used when fullscreen to hide/show the titlebar when the mouse is near the top of the window automatically. 239 | Items.Visibility = sender.IsVisible ? Visibility.Visible : Visibility.Collapsed; 240 | AppTitleBar.Visibility = Items.Visibility; 241 | } 242 | 243 | private void CoreTitleBar_LayoutMetricsChanged(CoreApplicationViewTitleBar sender, object args) 244 | { 245 | // Get the size of the caption controls area and back button 246 | // (returned in logical pixels), and move your content around as necessary. 247 | LeftPaddingColumn.Width = new GridLength(sender.SystemOverlayLeftInset); 248 | RightPaddingColumn.Width = new GridLength(sender.SystemOverlayRightInset); 249 | 250 | // Update title bar control size as needed to account for system size changes. 251 | AppTitleBar.Height = sender.Height; 252 | } 253 | #endregion 254 | 255 | #region Handle FullScreen 256 | private void MainPage_VisibleBoundsChanged(ApplicationView sender, object args) 257 | { 258 | // Update Fullscreen from other modes of adjusting view (keyboard shortcuts) 259 | IsFullScreen = ApplicationView.GetForCurrentView().IsFullScreenMode; 260 | } 261 | 262 | private void AppFullScreenShortcut(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args) 263 | { 264 | // Toggle FullScreen from F11 Keyboard Shortcut 265 | if (!IsFullScreen) 266 | { 267 | IsFullScreen = ApplicationView.GetForCurrentView().TryEnterFullScreenMode(); 268 | } 269 | else 270 | { 271 | ApplicationView.GetForCurrentView().ExitFullScreenMode(); 272 | IsFullScreen = false; 273 | } 274 | } 275 | 276 | private void Button_FullScreen_Click(object sender, RoutedEventArgs e) 277 | { 278 | // Redirect to our shortcut key. 279 | AppFullScreenShortcut(null, null); 280 | } 281 | #endregion 282 | 283 | private void Set(ref T storage, T value, [CallerMemberName]string propertyName = null) 284 | { 285 | if (Equals(storage, value)) 286 | { 287 | return; 288 | } 289 | 290 | storage = value; 291 | OnPropertyChanged(propertyName); 292 | } 293 | 294 | private void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 295 | 296 | private void InitializeTestData() 297 | { 298 | TabItems.Add(new DataItem() 299 | { 300 | Title = "Item 1", 301 | Content = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur a consectetur arcu, eu imperdiet nisl. Nunc id interdum odio. Aliquam non vulputate sem. Proin lacinia, lacus vitae finibus malesuada, leo libero interdum nisl, et dictum justo tortor semper tortor. Phasellus suscipit malesuada ultrices. Cras sodales vel lectus quis mattis. Sed consequat mollis ultrices. Nam eleifend purus sit amet massa mattis facilisis. Donec fringilla convallis nibh eget venenatis. Morbi ac venenatis ex. Integer ultrices velit eget dictum ultrices. Nunc aliquet lectus vitae feugiat varius. Nulla erat nisi, scelerisque ut sollicitudin id, vestibulum at mi. Donec neque velit, ornare consectetur aliquet id, egestas nec sapien. Nulla nec magna sed nunc varius bibendum." 302 | }); 303 | TabItems.Add(new DataItem() 304 | { 305 | Title = "Item 2", 306 | Content = "Aliquam fringilla euismod neque sit amet porta. Aliquam et ligula in neque ullamcorper interdum sit amet et magna. Quisque maximus accumsan lorem at rhoncus. Pellentesque mattis, eros non accumsan auctor, libero turpis sodales urna, id porta mi dolor at elit. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec lacinia leo arcu, vitae malesuada sapien consequat eget. Pellentesque vestibulum interdum convallis. Mauris nulla elit, tempus sit amet enim finibus, suscipit tempor ante. Nullam pulvinar libero sed tincidunt sagittis. Suspendisse potenti. Nulla porta lacinia lacus vel bibendum. Sed sagittis dignissim leo, ac gravida sem mattis pellentesque." 307 | }); 308 | TabItems.Add(new DataItem() 309 | { 310 | Title = "Item 3", 311 | Content = "Donec tellus nisl, volutpat vel urna eu, vestibulum sollicitudin sapien. Aliquam libero ex, egestas ut dapibus ullamcorper, mattis non nisl. Pellentesque quis hendrerit nibh. In lobortis placerat interdum. Aliquam et eleifend velit. Nunc ipsum orci, auctor eget eros non, euismod accumsan quam. Nam sit amet convallis est. Integer eget mauris pharetra, fringilla elit a, eleifend felis. Nullam vel ex posuere, blandit tellus nec, lobortis mauris. Nulla rhoncus nisi vel leo condimentum, non cursus lacus tempus." 312 | }); 313 | TabItems.Add(new DataItem() 314 | { 315 | Title = "Item 4", 316 | Content = "Nullam sollicitudin magna dui, imperdiet vulputate arcu pharetra eu. Vivamus lobortis lectus ut diam pretium, ut fermentum est malesuada. Sed eget pretium nisi. Cras eget vestibulum purus. Vivamus tincidunt luctus maximus. Cras erat enim, molestie sit amet tortor sit amet, porttitor tincidunt neque. Nam malesuada odio justo, sed sagittis tellus mollis in. Proin congue enim quis libero faucibus, eu condimentum dolor convallis. Mauris blandit ipsum sit amet maximus convallis. Integer porta dolor id purus hendrerit, a semper mi blandit. In malesuada lacus a tellus interdum, vel consequat turpis molestie. Curabitur eget venenatis massa." 317 | }); 318 | TabItems.Add(new DataItem() 319 | { 320 | Title = "Item 5", 321 | Content = "Etiam egestas, tellus ut molestie cursus, odio eros accumsan nulla, ut tempor libero nisi a ante. Sed posuere, velit id dictum lobortis, magna lorem dapibus urna, vitae mattis tellus libero et ligula. Praesent vel orci vehicula, accumsan ipsum ac, venenatis erat. Vestibulum consequat nulla eget arcu accumsan, tempus condimentum nulla euismod. Cras mattis tellus tortor, vitae vulputate lectus vulputate ac. Nunc nisl est, porttitor vitae diam a, pulvinar faucibus augue. Morbi vitae bibendum sem, non porta dolor. Cras turpis sem, rhoncus eget ultrices a, pretium venenatis libero. Fusce convallis eu sapien eu imperdiet. Nullam pulvinar ante a lobortis commodo. Aenean at est vel est faucibus efficitur in eget turpis. In efficitur bibendum dolor vitae dapibus. Mauris dapibus risus sit amet lectus ornare, et eleifend urna pretium. Integer non semper nibh, sit amet bibendum nulla. Nulla facilisi." 322 | }); 323 | TabItems.Add(new DataItem() 324 | { 325 | Title = "Item 6", 326 | Content = "Integer in pulvinar justo, non venenatis leo. Nam quis pulvinar libero, id laoreet elit. Nunc vehicula vitae lectus et venenatis. Etiam et porta dui. Nulla rutrum lacinia dolor. Nullam convallis libero eget nisi tristique, quis convallis enim finibus. Suspendisse consectetur lorem eleifend sem venenatis ultrices. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nunc ligula urna, aliquam vitae est a, dictum gravida nulla. Sed eu vestibulum nisl. Phasellus rhoncus volutpat mauris, vitae semper quam molestie et. Fusce mattis turpis a congue maximus. Suspendisse justo dui, varius non metus vel, euismod pretium velit." 327 | }); 328 | } 329 | } 330 | } 331 | --------------------------------------------------------------------------------