diff --git a/Microsoft.Toolkit.Services/AssemblyInfo.cs b/Microsoft.Toolkit.Services/AssemblyInfo.cs index 90ce2f74961..02efb81e9d2 100644 --- a/Microsoft.Toolkit.Services/AssemblyInfo.cs +++ b/Microsoft.Toolkit.Services/AssemblyInfo.cs @@ -11,4 +11,5 @@ // ****************************************************************** using System.Runtime.CompilerServices; -[assembly:InternalsVisibleTo("Microsoft.Toolkit.Uwp.Services")] \ No newline at end of file +[assembly:InternalsVisibleTo("Microsoft.Toolkit.Uwp.Services")] +[assembly:InternalsVisibleTo("Microsoft.Toolkit.Uwp.UI.Controls.Graph")] \ No newline at end of file diff --git a/Microsoft.Toolkit.Services/Services/MicrosoftGraph/MicrosoftGraphService.cs b/Microsoft.Toolkit.Services/Services/MicrosoftGraph/MicrosoftGraphService.cs index f2f1551b1e4..8918a09bf73 100644 --- a/Microsoft.Toolkit.Services/Services/MicrosoftGraph/MicrosoftGraphService.cs +++ b/Microsoft.Toolkit.Services/Services/MicrosoftGraph/MicrosoftGraphService.cs @@ -12,6 +12,7 @@ using System; using System.Net.Http.Headers; +using System.Threading; using System.Threading.Tasks; using Microsoft.Graph; using Microsoft.Identity.Client; @@ -24,11 +25,18 @@ namespace Microsoft.Toolkit.Services.MicrosoftGraph /// public class MicrosoftGraphService { + private readonly SemaphoreSlim _readLock = new SemaphoreSlim(1, 1); + /// /// Gets or sets Authentication instance. /// internal MicrosoftGraphAuthenticationHelper Authentication { get; set; } + /// + /// Event raised when user logs in our out. + /// + public event EventHandler IsAuthenticatedChanged; + /// /// Gets or sets store a reference to an instance of the underlying data provider. /// @@ -49,10 +57,27 @@ public class MicrosoftGraphService /// protected bool IsInitialized { get; set; } + private bool _isAuthenticated; + /// /// Gets or sets a value indicating whether user is connected. /// - protected bool IsConnected { get; set; } + public bool IsAuthenticated + { + get + { + return _isAuthenticated; + } + + protected set + { + if (_isAuthenticated != value) + { + _isAuthenticated = value; + IsAuthenticatedChanged?.Invoke(this, EventArgs.Empty); + } + } + } /// /// Gets or sets AppClientId. @@ -175,6 +200,10 @@ public virtual Task Logout() { throw new InvalidOperationException("Microsoft Graph not initialized."); } + + IsAuthenticated = false; + User = null; + #if WINRT var authenticationModel = AuthenticationModel.ToString(); return Authentication.LogoutAsync(authenticationModel); @@ -190,7 +219,6 @@ public virtual Task Logout() /// Returns success or failure of login attempt. public virtual async Task LoginAsync(string loginHint = null) { - IsConnected = false; if (!IsInitialized) { throw new InvalidOperationException("Microsoft Graph not initialized."); @@ -213,11 +241,10 @@ public virtual async Task LoginAsync(string loginHint = null) if (string.IsNullOrEmpty(accessToken)) { - return IsConnected; + IsAuthenticated = false; + return IsAuthenticated; } - IsConnected = true; - #if WINRT User = new MicrosoftGraphUserService(GraphProvider); #else @@ -239,7 +266,85 @@ public virtual async Task LoginAsync(string loginHint = null) User.InitializeEvent(); } - return IsConnected; + IsAuthenticated = true; + return IsAuthenticated; + } + + /// + /// Tries to log in user if not already loged in + /// + /// true if service is already loged in + internal async Task TryLoginAsync() + { + if (!IsInitialized) + { + return false; + } + + if (IsAuthenticated) + { + return true; + } + + try + { + await _readLock.WaitAsync(); + await LoginAsync(); + } + catch (MsalServiceException ex) + { + // Swallow error in case of authentication cancellation. + if (ex.ErrorCode != "authentication_canceled" + && ex.ErrorCode != "access_denied") + { + throw ex; + } + } + finally + { + _readLock.Release(); + } + + return IsAuthenticated; + } + + internal async Task ConnectForAnotherUserAsync() + { + if (!IsInitialized) + { + throw new InvalidOperationException("Microsoft Graph not initialized."); + } + + try + { + var publicClientApplication = new PublicClientApplication(AppClientId); + AuthenticationResult result = await publicClientApplication.AcquireTokenAsync(DelegatedPermissionScopes); + + var signedUser = result.User; + + foreach (var user in publicClientApplication.Users) + { + if (user.Identifier != signedUser.Identifier) + { + publicClientApplication.Remove(user); + } + } + + await LoginAsync(); + + return true; + } + catch (MsalServiceException ex) + { + // Swallow error in case of authentication cancellation. + if (ex.ErrorCode != "authentication_canceled" + && ex.ErrorCode != "access_denied") + { + throw ex; + } + } + + return false; } /// diff --git a/Microsoft.Toolkit.Uwp.SampleApp/App.xaml b/Microsoft.Toolkit.Uwp.SampleApp/App.xaml index 0309871e102..3d8f3de563e 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/App.xaml +++ b/Microsoft.Toolkit.Uwp.SampleApp/App.xaml @@ -1,17 +1,17 @@  + xmlns:toolkitControls="using:Microsoft.Toolkit.Uwp.UI.Controls" + RequestedTheme="Light" + RequiresPointerMode="Auto"> @@ -26,7 +26,8 @@ #FFFF0000 #3750D1 - + - + + Color="{StaticResource Brand-Color}" /> - + - + HorizontalScrollBarVisibility="Auto" + HorizontalScrollMode="Auto"> + Padding="10" + FontFamily="Consolas" /> - - - + ShadowOpacity="0.6" + Visibility="Collapsed"> + - + - + - - + + - - - + + TabIndex="2"> @@ -839,38 +890,47 @@ - + - + - + ShadowOpacity="0.6" + Color="Black"> @@ -901,14 +961,14 @@ - + - + HorizontalAlignment="Left" + Background="{StaticResource Brush-Blue-01}" + Opacity="1" + Visibility="{Binding BadgeUpdateVersionRequired, Converter={StaticResource EmptyStringToObject}}"> - + - - + + - - + + - - + + - - + + @@ -972,7 +1055,7 @@ - + @@ -995,7 +1078,8 @@ - - + diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/AadLogin/SignInEventArgs.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/AadLogin/SignInEventArgs.cs new file mode 100644 index 00000000000..a038a7ba6ef --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/AadLogin/SignInEventArgs.cs @@ -0,0 +1,34 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE 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 CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +using Microsoft.Graph; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Graph +{ + /// + /// Arguments relating to a sign-in event of Aadlogin control + /// + public class SignInEventArgs + { + internal SignInEventArgs() + { + } + + /// + /// Gets the graph service client with authorized token. + /// + public GraphServiceClient GraphClient + { + get; internal set; + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/Assets/folder.svg b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/Assets/folder.svg new file mode 100644 index 00000000000..bd9a64876bd --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/Assets/folder.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/Assets/genericfile.png b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/Assets/genericfile.png new file mode 100644 index 00000000000..bf43bc2b680 Binary files /dev/null and b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/Assets/genericfile.png differ diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/Assets/person.png b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/Assets/person.png new file mode 100644 index 00000000000..35668622a7d Binary files /dev/null and b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/Assets/person.png differ diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/Assets/photo.png b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/Assets/photo.png new file mode 100644 index 00000000000..ca21eb5c049 Binary files /dev/null and b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/Assets/photo.png differ diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/Microsoft.Toolkit.Uwp.UI.Controls.Graph.csproj b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/Microsoft.Toolkit.Uwp.UI.Controls.Graph.csproj new file mode 100644 index 00000000000..2974c91533a --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/Microsoft.Toolkit.Uwp.UI.Controls.Graph.csproj @@ -0,0 +1,42 @@ + + + + uap10.0 + Windows Community Toolkit Graph Controls + This library provides Microsoft Graph XAML controls .It is part of the Windows Community Toolkit. + UWP Toolkit Windows Controls Microsoft Graph AadLogin ProfileCard PeoplePicker SharePointFiles + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/PeoplePicker/PeoplePicker.Events.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/PeoplePicker/PeoplePicker.Events.cs new file mode 100644 index 00000000000..ca13e44adb6 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/PeoplePicker/PeoplePicker.Events.cs @@ -0,0 +1,152 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE 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 CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Graph; +using Microsoft.Toolkit.Services.MicrosoftGraph; +using Microsoft.Toolkit.Uwp.UI.Extensions; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Graph +{ + /// + /// Defines the events for the control. + /// + public partial class PeoplePicker : Control + { + private static void AllowMultiplePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = d as PeoplePicker; + if (!control.AllowMultiple) + { + control.Selections.Clear(); + control.RaiseSelectionChanged(); + control._searchBox.Text = string.Empty; + } + } + + private void ClearAndHideSearchResultListBox() + { + SearchResultList.Clear(); + _searchResultListBox.Visibility = Visibility.Collapsed; + } + + private async void SearchBox_OnTextChanged(object sender, TextChangedEventArgs e) + { + var textboxSender = (TextBox)sender; + string searchText = textboxSender.Text.Trim(); + if (string.IsNullOrWhiteSpace(searchText)) + { + ClearAndHideSearchResultListBox(); + return; + } + + IsLoading = true; + try + { + var graphService = MicrosoftGraphService.Instance; + await graphService.TryLoginAsync(); + GraphServiceClient graphClient = graphService.GraphProvider; + + if (graphClient != null) + { + var options = new List + { + new QueryOption("$search", searchText) + }; + IUserPeopleCollectionPage peopleList = await graphClient.Me.People.Request(options).GetAsync(); + + if (peopleList.Any()) + { + List searchResult = peopleList.Where( + u => !string.IsNullOrWhiteSpace(u.UserPrincipalName)).ToList(); + + // Remove all selected items + foreach (Person selectedItem in Selections) + { + searchResult.RemoveAll(u => u.UserPrincipalName == selectedItem.UserPrincipalName); + } + + SearchResultList.Clear(); + var result = SearchResultLimit > 0 + ? searchResult.Take(SearchResultLimit).ToList() + : searchResult; + foreach (var item in result) + { + SearchResultList.Add(item); + } + + _searchResultListBox.Visibility = Visibility.Visible; + } + else + { + ClearAndHideSearchResultListBox(); + } + } + } + catch (Exception) + { + } + finally + { + IsLoading = false; + } + } + + private void SearchResultListBox_OnSelectionChanged(object sender, Windows.UI.Xaml.Controls.SelectionChangedEventArgs e) + { +#pragma warning disable SA1119 // Statement must not use unnecessary parenthesis + if (!((sender as ListBox)?.SelectedItem is Person person)) +#pragma warning restore SA1119 // Statement must not use unnecessary parenthesis + { + return; + } + + if (!AllowMultiple && Selections.Any()) + { + Selections.Clear(); + Selections.Add(person); + } + else + { + Selections.Add(person); + } + + RaiseSelectionChanged(); + + _searchBox.Text = string.Empty; + } + + private void SelectionsListBox_Tapped(object sender, Windows.UI.Xaml.Input.TappedRoutedEventArgs e) + { + var elem = e.OriginalSource as FrameworkElement; + + var removeButton = elem.FindAscendantByName("PersonRemoveButton"); + if (removeButton != null) + { + if (removeButton.Tag is Person item) + { + Selections.Remove(item); + RaiseSelectionChanged(); + } + } + } + + private void RaiseSelectionChanged() + { + this.SelectionChanged?.Invoke(this, new PeopleSelectionChangedEventArgs(this.Selections)); + } + } +} \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/PeoplePicker/PeoplePicker.Properties.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/PeoplePicker/PeoplePicker.Properties.cs new file mode 100644 index 00000000000..88223819c99 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/PeoplePicker/PeoplePicker.Properties.cs @@ -0,0 +1,147 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE 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 CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +using System; +using System.Collections.ObjectModel; +using Microsoft.Graph; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Graph +{ + /// + /// Defines the properties for the control. + /// + public partial class PeoplePicker : Control + { + /// + /// File is selected + /// + public event EventHandler SelectionChanged; + + /// + /// Gets required delegated permissions for the control + /// + public static string[] RequiredDelegatedPermissions + { + get + { + return new string[] { "User.Read", "User.ReadBasic.All" }; + } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty AllowMultipleProperty = + DependencyProperty.Register( + nameof(AllowMultiple), + typeof(bool), + typeof(PeoplePicker), + new PropertyMetadata(true, AllowMultiplePropertyChanged)); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty SearchResultLimitProperty = + DependencyProperty.Register( + nameof(SearchResultLimit), + typeof(int), + typeof(PeoplePicker), + null); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty PlaceholderTextProperty = + DependencyProperty.Register( + nameof(PlaceholderText), + typeof(string), + typeof(PeoplePicker), + new PropertyMetadata("Enter keywords to search people")); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty SelectionsProperty = + DependencyProperty.Register( + nameof(Selections), + typeof(ObservableCollection), + typeof(PeoplePicker), + null); + + /// + /// Identifies the dependency property. + /// + internal static readonly DependencyProperty SearchResultListProperty = + DependencyProperty.Register( + nameof(SearchResultList), + typeof(ObservableCollection), + typeof(PeoplePicker), + null); + + private static readonly DependencyProperty IsLoadingProperty = + DependencyProperty.Register( + nameof(IsLoading), + typeof(bool), + typeof(PeoplePicker), + null); + + /// + /// Gets or sets a value indicating whether multiple people can be selected + /// + public bool AllowMultiple + { + get => (bool)GetValue(AllowMultipleProperty); + set => SetValue(AllowMultipleProperty, value); + } + + /// + /// Gets or sets the max person returned in the search results + /// + public int SearchResultLimit + { + get => (int)GetValue(SearchResultLimitProperty); + set => SetValue(SearchResultLimitProperty, value); + } + + /// + /// Gets or sets the text to be displayed when no user is selected + /// + public string PlaceholderText + { + get => (string)GetValue(PlaceholderTextProperty); + set => SetValue(PlaceholderTextProperty, value); + } + + /// + /// Gets or sets the selected person list. + /// + public ObservableCollection Selections + { + get => (ObservableCollection)GetValue(SelectionsProperty); + set => SetValue(SelectionsProperty, value); + } + + internal ObservableCollection SearchResultList + { + get => (ObservableCollection)GetValue(SearchResultListProperty); + set => SetValue(SearchResultListProperty, value); + } + + private bool IsLoading + { + get => (bool)GetValue(IsLoadingProperty); + set => SetValue(IsLoadingProperty, value); + } + } +} \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/PeoplePicker/PeoplePicker.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/PeoplePicker/PeoplePicker.cs new file mode 100644 index 00000000000..061e15b7a95 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/PeoplePicker/PeoplePicker.cs @@ -0,0 +1,75 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE 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 CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +using System.Collections.ObjectModel; +using Microsoft.Graph; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Graph +{ + /// + /// The PeoplePicker Control is a simple control that allows for selection of one or more users from an organizational AD. + /// + [TemplatePart(Name = SearchBoxPartName, Type = typeof(TextBox))] + [TemplatePart(Name = SearchResultListBoxPartName, Type = typeof(ListBox))] + [TemplatePart(Name = SelectionsListBoxPartName, Type = typeof(ListBox))] + public partial class PeoplePicker : Control + { + private const string SearchBoxPartName = "SearchBox"; + private const string SearchResultListBoxPartName = "SearchResultListBox"; + private const string SelectionsListBoxPartName = "SelectionsListBox"; + + private TextBox _searchBox; + private ListBox _searchResultListBox; + private ListBox _selectionsListBox; + + /// + /// Initializes a new instance of the class. + /// + public PeoplePicker() + { + DefaultStyleKey = typeof(PeoplePicker); + } + + /// + /// Called when applying the control template. + /// + protected override void OnApplyTemplate() + { + IsLoading = false; + + _searchBox = GetTemplateChild(SearchBoxPartName) as TextBox; + _searchResultListBox = GetTemplateChild(SearchResultListBoxPartName) as ListBox; + _selectionsListBox = GetTemplateChild(SelectionsListBoxPartName) as ListBox; + + SearchResultList = new ObservableCollection(); + Selections = Selections ?? new ObservableCollection(); + if (_searchBox != null) + { + _searchBox.TextChanged += SearchBox_OnTextChanged; + } + + if (_searchResultListBox != null) + { + _searchResultListBox.SelectionChanged += SearchResultListBox_OnSelectionChanged; + } + + if (_selectionsListBox != null) + { + _selectionsListBox.Tapped += SelectionsListBox_Tapped; + } + + base.OnApplyTemplate(); + } + } +} \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/PeoplePicker/PeoplePicker.xaml b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/PeoplePicker/PeoplePicker.xaml new file mode 100644 index 00000000000..2e2195a40af --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/PeoplePicker/PeoplePicker.xaml @@ -0,0 +1,90 @@ + + + + + + + + selected + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/PeoplePicker/PeopleSelectionChangedEventArgs.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/PeoplePicker/PeopleSelectionChangedEventArgs.cs new file mode 100644 index 00000000000..d5d203acb83 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/PeoplePicker/PeopleSelectionChangedEventArgs.cs @@ -0,0 +1,35 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE 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 CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +using System; +using Microsoft.Graph; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Graph +{ + using System.Collections.ObjectModel; + + /// + /// Arguments relating to the people selected event of control + /// + public class PeopleSelectionChangedEventArgs + { + /// + /// Gets selected file + /// + public ObservableCollection Selections { get; private set; } + + internal PeopleSelectionChangedEventArgs(ObservableCollection selections) + { + Selections = selections; + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/ProfileCard/ProfileCard.Events.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/ProfileCard/ProfileCard.Events.cs new file mode 100644 index 00000000000..bcca255731b --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/ProfileCard/ProfileCard.Events.cs @@ -0,0 +1,48 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE 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 CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +using Microsoft.Toolkit.Services.MicrosoftGraph; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Graph +{ + /// + /// Defines the events for the control. + /// + public partial class ProfileCard : Control + { + private static void OnUserIdPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + => (d as ProfileCard).FetchUserInfo(); + + private static void OnDisplayModePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var profileCard = d as ProfileCard; + ProfileCardItem profileItem = profileCard.CurrentProfileItem.Clone(); + profileItem.DisplayMode = (ViewType)e.NewValue; + profileCard.CurrentProfileItem = profileItem; + } + + private static void OnDefaultValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var profileCard = d as ProfileCard; + var graphService = MicrosoftGraphService.Instance; + + if (!graphService.IsAuthenticated + || string.IsNullOrEmpty(profileCard.UserId) + || profileCard.UserId.Equals("Invalid UserId")) + { + profileCard.InitUserProfile(); + } + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/ProfileCard/ProfileCard.Properties.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/ProfileCard/ProfileCard.Properties.cs new file mode 100644 index 00000000000..8387902c0f8 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/ProfileCard/ProfileCard.Properties.cs @@ -0,0 +1,155 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE 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 CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Media.Imaging; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Graph +{ + /// + /// Defines the properties for the control. + /// + public partial class ProfileCard : Control + { + /// + /// Gets required delegated permissions for the control + /// + public static string[] RequiredDelegatedPermissions + { + get + { + return new string[] { "User.Read", "User.ReadBasic.All" }; + } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty UserIdProperty = DependencyProperty.Register( + nameof(UserId), + typeof(string), + typeof(ProfileCard), + new PropertyMetadata(string.Empty, OnUserIdPropertyChanged)); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty DisplayModeProperty = DependencyProperty.Register( + nameof(DisplayMode), + typeof(ViewType), + typeof(ProfileCard), + new PropertyMetadata(ViewType.PictureOnly, OnDisplayModePropertyChanged)); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty DefaultImageProperty = DependencyProperty.Register( + nameof(DefaultImage), + typeof(BitmapImage), + typeof(ProfileCard), + new PropertyMetadata(null, OnDefaultValuePropertyChanged)); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty LargeProfileTitleDefaultTextProperty = DependencyProperty.Register( + nameof(LargeProfileTitleDefaultText), + typeof(string), + typeof(ProfileCard), + new PropertyMetadata(string.Empty, OnDefaultValuePropertyChanged)); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty LargeProfileMailDefaultTextProperty = DependencyProperty.Register( + nameof(LargeProfileMailDefaultText), + typeof(string), + typeof(ProfileCard), + new PropertyMetadata(string.Empty, OnDefaultValuePropertyChanged)); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty NormalMailDefaultTextProperty = DependencyProperty.Register( + nameof(NormalMailDefaultText), + typeof(string), + typeof(ProfileCard), + new PropertyMetadata(string.Empty, OnDefaultValuePropertyChanged)); + + internal static readonly DependencyProperty CurrentProfileItemProperty = DependencyProperty.Register( + nameof(CurrentProfileItem), + typeof(ProfileCardItem), + typeof(ProfileCard), + new PropertyMetadata(new ProfileCardItem())); + + /// + /// Gets or sets user unique identifier. + /// + public string UserId + { + get { return ((string)GetValue(UserIdProperty))?.Trim(); } + set { SetValue(UserIdProperty, value?.Trim()); } + } + + /// + /// Gets or sets the visual layout of the control. Default is PictureOnly. + /// + public ViewType DisplayMode + { + get { return (ViewType)GetValue(DisplayModeProperty); } + set { SetValue(DisplayModeProperty, value); } + } + + /// + /// Gets or sets the default image when no user is signed in. + /// + public BitmapImage DefaultImage + { + get { return (BitmapImage)GetValue(DefaultImageProperty); } + set { SetValue(DefaultImageProperty, value); } + } + + /// + /// Gets or sets the default title text in LargeProfilePhotoLeft mode or LargeProfilePhotoRight mode when no user is signed in. + /// + public string LargeProfileTitleDefaultText + { + get { return (string)GetValue(LargeProfileTitleDefaultTextProperty); } + set { SetValue(LargeProfileTitleDefaultTextProperty, value); } + } + + /// + /// Gets or sets the default secondary mail text in LargeProfilePhotoLeft mode or LargeProfilePhotoRight mode when no user is signed in. + /// + public string LargeProfileMailDefaultText + { + get { return (string)GetValue(LargeProfileMailDefaultTextProperty); } + set { SetValue(LargeProfileMailDefaultTextProperty, value); } + } + + /// + /// Gets or sets the default mail text in EmailOnly mode when no user is signed in. + /// + public string NormalMailDefaultText + { + get { return (string)GetValue(NormalMailDefaultTextProperty); } + set { SetValue(NormalMailDefaultTextProperty, value); } + } + + internal ProfileCardItem CurrentProfileItem + { + get { return (ProfileCardItem)GetValue(CurrentProfileItemProperty); } + set { SetValue(CurrentProfileItemProperty, value); } + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/ProfileCard/ProfileCard.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/ProfileCard/ProfileCard.cs new file mode 100644 index 00000000000..ccff427b892 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/ProfileCard/ProfileCard.cs @@ -0,0 +1,138 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE 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 CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +using System; +using System.IO; +using Microsoft.Graph; +using Microsoft.Toolkit.Services.MicrosoftGraph; +using Windows.UI.Core; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Media.Imaging; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Graph +{ + /// + /// The Profile Card control is a simple way to display a user in multiple different formats and mixes of name/image/e-mail. + /// + public partial class ProfileCard : Control + { + private static readonly BitmapImage PersonPhoto = new BitmapImage(new Uri("ms-appx:///Microsoft.Toolkit.Uwp.UI.Controls.Graph/Assets/person.png")); + private ContentControl _contentPresenter; + + /// + /// Initializes a new instance of the class. + /// + public ProfileCard() + { + DefaultStyleKey = typeof(ProfileCard); + } + + /// + /// Override default OnApplyTemplate to initialize child controls + /// + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + _contentPresenter = GetTemplateChild("ContentPresenter") as ContentControl; + if (_contentPresenter != null) + { + _contentPresenter.ContentTemplateSelector = new ProfileDisplayModeTemplateSelector(_contentPresenter); + } + + FetchUserInfo(); + } + + private async void FetchUserInfo() + { + if (string.IsNullOrEmpty(UserId) || UserId.Equals("Invalid UserId")) + { + InitUserProfile(); + } + else + { + var graphService = MicrosoftGraphService.Instance; + if (!(await graphService.TryLoginAsync())) + { + return; + } + + try + { + var user = await graphService.GraphProvider.Users[UserId].Request().GetAsync(); + var profileItem = new ProfileCardItem() + { + NormalMail = user.Mail, + LargeProfileTitle = user.DisplayName, + LargeProfileMail = user.Mail, + DisplayMode = DisplayMode + }; + + if (string.IsNullOrEmpty(user.Mail)) + { + profileItem.UserPhoto = DefaultImage ?? PersonPhoto; + } + else + { + try + { + using (Stream photoStream = await graphService.GraphProvider.Users[UserId].Photo.Content.Request().GetAsync()) + using (var ras = photoStream.AsRandomAccessStream()) + { + var bitmapImage = new BitmapImage(); + await bitmapImage.SetSourceAsync(ras); + profileItem.UserPhoto = bitmapImage; + } + } + catch + { + // Swallow error in case of no photo found + profileItem.UserPhoto = DefaultImage ?? PersonPhoto; + } + } + + await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => + { + CurrentProfileItem = profileItem; + }); + } + catch (ServiceException ex) + { + // Swallow error in case of no user id found + if (!ex.Error.Code.Equals("Request_ResourceNotFound")) + { + throw; + } + + await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => + { + UserId = "Invalid UserId"; + }); + } + } + } + + private void InitUserProfile() + { + var profileItem = new ProfileCardItem() + { + UserPhoto = DefaultImage ?? PersonPhoto, + NormalMail = NormalMailDefaultText ?? string.Empty, + LargeProfileTitle = LargeProfileTitleDefaultText ?? string.Empty, + LargeProfileMail = LargeProfileMailDefaultText ?? string.Empty, + DisplayMode = DisplayMode + }; + + CurrentProfileItem = profileItem; + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/ProfileCard/ProfileCard.xaml b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/ProfileCard/ProfileCard.xaml new file mode 100644 index 00000000000..e4a2ca98388 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/ProfileCard/ProfileCard.xaml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/ProfileCard/ProfileCardItem.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/ProfileCard/ProfileCardItem.cs new file mode 100644 index 00000000000..6439d9df0ab --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/ProfileCard/ProfileCardItem.cs @@ -0,0 +1,34 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE 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 CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +using Windows.UI.Xaml.Media.Imaging; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Graph +{ + internal class ProfileCardItem + { + public string NormalMail { get; set; } + + public string LargeProfileTitle { get; set; } + + public string LargeProfileMail { get; set; } + + public BitmapImage UserPhoto { get; set; } + + public ViewType DisplayMode { get; set; } + + public ProfileCardItem Clone() + { + return (ProfileCardItem)MemberwiseClone(); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/ProfileCard/ProfileDisplayModeTemplateSelector.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/ProfileCard/ProfileDisplayModeTemplateSelector.cs new file mode 100644 index 00000000000..c00cc33fdd0 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/ProfileCard/ProfileDisplayModeTemplateSelector.cs @@ -0,0 +1,63 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE 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 CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Graph +{ + internal class ProfileDisplayModeTemplateSelector : DataTemplateSelector + { + private ContentControl _contentPresenter; + + internal ProfileDisplayModeTemplateSelector(ContentControl contentPresenter) + { + _contentPresenter = contentPresenter; + } + + private DataTemplate SelectItemTemplate(object item) + { + DataTemplate dataTemplate = null; + + if (item != null && item is ProfileCardItem profileItem) + { + switch (profileItem.DisplayMode) + { + case ViewType.EmailOnly: + dataTemplate = _contentPresenter.Resources["EmailOnly"] as DataTemplate; + break; + case ViewType.PictureOnly: + dataTemplate = _contentPresenter.Resources["PictureOnly"] as DataTemplate; + break; + case ViewType.LargeProfilePhotoLeft: + dataTemplate = _contentPresenter.Resources["LargeProfilePhotoLeft"] as DataTemplate; + break; + case ViewType.LargeProfilePhotoRight: + dataTemplate = _contentPresenter.Resources["LargeProfilePhotoRight"] as DataTemplate; + break; + } + } + + return dataTemplate; + } + + protected override DataTemplate SelectTemplateCore(object item) + { + return SelectItemTemplate(item); + } + + protected override DataTemplate SelectTemplateCore(object item, DependencyObject container) + { + return SelectItemTemplate(item); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/ProfileCard/ViewType.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/ProfileCard/ViewType.cs new file mode 100644 index 00000000000..51b4542b171 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/ProfileCard/ViewType.cs @@ -0,0 +1,40 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE 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 CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Graph +{ + /// + /// The visual layout of the control. Default is PictureOnly. + /// + public enum ViewType + { + /// + /// Only the user photo is shown. + /// + PictureOnly = 0, + + /// + /// Only the user email is shown. + /// + EmailOnly = 1, + + /// + /// A basic user profile is shown, and the user photo is place on the left side. + /// + LargeProfilePhotoLeft = 2, + + /// + /// A basic user profile is shown, and the user photo is place on the right side. + /// + LargeProfilePhotoRight = 3 + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/Properties/AssemblyInfo.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..d468edfac79 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/Properties/AssemblyInfo.cs @@ -0,0 +1,18 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE 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 CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +using System.Runtime.CompilerServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: InternalsVisibleTo("UnitTests")] \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/Properties/Microsoft.Windows.Toolkit.UI.Controls.Graph.rd.xml b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/Properties/Microsoft.Windows.Toolkit.UI.Controls.Graph.rd.xml new file mode 100644 index 00000000000..9ea5a951ee2 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/Properties/Microsoft.Windows.Toolkit.UI.Controls.Graph.rd.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/DetailPaneDisplayMode.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/DetailPaneDisplayMode.cs new file mode 100644 index 00000000000..f50b416572f --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/DetailPaneDisplayMode.cs @@ -0,0 +1,40 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE 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 CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Graph +{ + /// + /// Determines how file details panel is displayed in the control. + /// + public enum DetailPaneDisplayMode + { + /// + /// Hide show DetailPane + /// + Disabled, + + /// + /// Show DetailPane aside + /// + Side, + + /// + /// Show DetailPane at bottom + /// + Bottom, + + /// + /// Show DetailPane over list + /// + Full + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/DriveItemIconConverter.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/DriveItemIconConverter.cs new file mode 100644 index 00000000000..e92ad39d358 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/DriveItemIconConverter.cs @@ -0,0 +1,68 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE 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 CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +using System; +using Microsoft.Graph; +using Windows.UI.Xaml.Data; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Graph +{ + /// + /// Get icon of DriveItem + /// + internal class DriveItemIconConverter : IValueConverter + { + private static readonly string OfficeIcon = "https://static2.sharepointonline.com/files/fabric/assets/brand-icons/document/png/{0}_32x1_5.png"; + private static readonly string LocalIcon = "ms-appx:///Microsoft.Toolkit.Uwp.UI.Controls.Graph/Assets/{0}"; + + public object Convert(object value, Type targetType, object parameter, string language) + { + DriveItem driveItem = value as DriveItem; + + if (driveItem.Folder != null) + { + return string.Format(LocalIcon, "folder.svg"); + } + else if (driveItem.File != null) + { + if (driveItem.File.MimeType.StartsWith("image")) + { + return string.Format(LocalIcon, "photo.png"); + } + else if (driveItem.File.MimeType.StartsWith("application/vnd.openxmlformats-officedocument")) + { + int index = driveItem.Name.LastIndexOf('.'); + if (index != -1) + { + string ext = driveItem.Name.Substring(index + 1); + return string.Format(OfficeIcon, ext); + } + } + } + else if (driveItem.Package != null) + { + switch (driveItem.Package.Type) + { + case "oneNote": + return string.Format(OfficeIcon, "one"); + } + } + + return string.Format(LocalIcon, "genericfile.png"); + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + return null; + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/FileSelectedEventArgs.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/FileSelectedEventArgs.cs new file mode 100644 index 00000000000..a0ffa01dc0b --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/FileSelectedEventArgs.cs @@ -0,0 +1,33 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE 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 CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +using System; +using Microsoft.Graph; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Graph +{ + /// + /// Arguments relating to a file selected event of SharePointFiles control + /// + public class FileSelectedEventArgs + { + /// + /// Gets selected file + /// + public DriveItem FileSelected { get; private set; } + + internal FileSelectedEventArgs(DriveItem fileSelected) + { + FileSelected = fileSelected; + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/FileSizeConverter.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/FileSizeConverter.cs new file mode 100644 index 00000000000..f392c1831fa --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/FileSizeConverter.cs @@ -0,0 +1,32 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE 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 CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +using System; +using Microsoft.Toolkit.Extensions; +using Windows.UI.Xaml.Data; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Graph +{ + internal class FileSizeConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, string language) + { + long size = (long)value; + return size.ToFileSizeString(); + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotImplementedException(); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/Int64Extensions.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/Int64Extensions.cs new file mode 100644 index 00000000000..bac3a7fa3fb --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/Int64Extensions.cs @@ -0,0 +1,57 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE 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 CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// All common long extensions should go here + /// + internal static class Int64Extensions + { + /// + /// Translate numeric file size to string format. + /// + /// file size in bytes. + /// Returns file size string. + public static string ToFileSizeString(this long size) + { + if (size < 1024) + { + return size.ToString("F0") + " bytes"; + } + else if ((size >> 10) < 1024) + { + return (size / (float)1024).ToString("F1") + " KB"; + } + else if ((size >> 20) < 1024) + { + return ((size >> 10) / (float)1024).ToString("F1") + " MB"; + } + else if ((size >> 30) < 1024) + { + return ((size >> 20) / (float)1024).ToString("F1") + " GB"; + } + else if ((size >> 40) < 1024) + { + return ((size >> 30) / (float)1024).ToString("F1") + " TB"; + } + else if ((size >> 50) < 1024) + { + return ((size >> 40) / (float)1024).ToString("F1") + " PB"; + } + else + { + return ((size >> 50) / (float)1024).ToString("F0") + " EB"; + } + } + } +} \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/SharePointFileList.Constants.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/SharePointFileList.Constants.cs new file mode 100644 index 00000000000..ec2dd0d5df4 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/SharePointFileList.Constants.cs @@ -0,0 +1,135 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE 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 CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Graph +{ + /// + /// The SharePointFiles Control displays a simple list of SharePoint Files. + /// + public partial class SharePointFileList + { + /// + /// Key of the VisualStateGroup to control nav buttons + /// + private const string NavStates = "NavStates"; + + /// + /// Key of the VisualState when display folder in readonly mode + /// + private const string NavStatesFolderReadonly = "FolderReadOnly"; + + /// + /// Key of the VisualState when display folder in edit mode + /// + private const string NavStatesFolderEdit = "FolderEdit"; + + /// + /// Key of the VisualState when display file in readonly mode + /// + private const string NavStatesFileReadonly = "FileReadonly"; + + /// + /// Key of the VisualState when display file in edit mode + /// + private const string NavStatesFileEdit = "FileEdit"; + + /// + /// Key of the VisualStateGroup to control uploading status + /// + private const string UploadStatus = "UploadStatus"; + + /// + /// Key of the VisualState when not uploading files + /// + private const string UploadStatusNotUploading = "NotUploading"; + + /// + /// Key of the VisualState when uploading files + /// + private const string UploadStatusUploading = "Uploading"; + + /// + /// Key of the VisualState when uploading error occurs + /// + private const string UploadStatusError = "Error"; + + /// + /// Key of the VisualStateGroup to control detail pane + /// + private const string DetailPaneStates = "DetailPaneStates"; + + /// + /// Key of the VisualState when detail pane is hidden + /// + private const string DetailPaneStatesHide = "Hide"; + + /// + /// Key of the VisualState when detail pane is at right side + /// + private const string DetailPaneStatesSide = "Side"; + + /// + /// Key of the VisualState when detail pane is at bottom + /// + private const string DetailPaneStatesBottom = "Bottom"; + + /// + /// Key of the VisualState when detail pane is in full mode + /// + private const string DetailPaneStatesFull = "Full"; + + /// + /// Key of the ListView that contains file list + /// + private const string ControlFileList = "list"; + + /// + /// Key of the back button that contains file list + /// + private const string ControlBack = "back"; + + /// + /// Key of the upload button that contains file list + /// + private const string ControlUpload = "upload"; + + /// + /// Key of the share button that contains file list + /// + private const string ControlShare = "share"; + + /// + /// Key of the download button that contains file list + /// + private const string ControlDownload = "download"; + + /// + /// Key of the delete button that contains file list + /// + private const string ControlDelete = "delete"; + + /// + /// Key of the error button that contains file list + /// + private const string ControlError = "error"; + + /// + /// Key of the cancel button that contains file list + /// + private const string ControlCancel = "cancel"; + + /// + /// Key of the has more button that contains file list + /// + private const string ControlLoadMore = "hasMore"; + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/SharePointFileList.Events.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/SharePointFileList.Events.cs new file mode 100644 index 00000000000..4b0df39a5f6 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/SharePointFileList.Events.cs @@ -0,0 +1,301 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE 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 CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Graph; +using Microsoft.Toolkit.Services.MicrosoftGraph; +using Windows.ApplicationModel.DataTransfer; +using Windows.Storage; +using Windows.Storage.Pickers; +using Windows.UI.Popups; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Media.Imaging; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Graph +{ + /// + /// The SharePointFiles Control displays a simple list of SharePoint Files. + /// + public partial class SharePointFileList + { + private static async void DriveUrlPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (MicrosoftGraphService.Instance.IsAuthenticated) + { + SharePointFileList control = d as SharePointFileList; + await MicrosoftGraphService.Instance.TryLoginAsync(); + GraphServiceClient graphServiceClient = MicrosoftGraphService.Instance.GraphProvider; + if (graphServiceClient != null && !string.IsNullOrWhiteSpace(control.DriveUrl)) + { + if (Uri.IsWellFormedUriString(control.DriveUrl, UriKind.Absolute)) + { + await control.InitDriveAsync(control.DriveUrl); + } + } + } + } + + private static void DetailPanePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + SharePointFileList control = d as SharePointFileList; + if (control.IsDetailPaneVisible) + { + control.ShowDetailsPane(); + } + } + + private async void Back_Click(object sender, RoutedEventArgs e) + { + if (DetailPane == DetailPaneDisplayMode.Full && IsDetailPaneVisible) + { + IsDetailPaneVisible = false; + HideDetailsPane(); + } + else if (_driveItemPath.Count > 1) + { + _driveItemPath.Pop(); + string parentItemId = _driveItemPath.Peek().Id; + if (_driveItemPath.Count == 1) + { + BackButtonVisibility = Visibility.Collapsed; + } + + UpdateCurrentPath(); + await LoadFilesAsync(parentItemId); + } + } + + private async void Upload_Click(object sender, RoutedEventArgs e) + { + ErrorMessage = string.Empty; + FileOpenPicker picker = new FileOpenPicker(); + picker.FileTypeFilter.Add("*"); + StorageFile file = await picker.PickSingleFileAsync(); + if (file != null) + { + string driveItemId = _driveItemPath.Peek()?.Id; + using (Stream inputStream = await file.OpenStreamForReadAsync()) + { + if (inputStream.Length < 1024 * 1024 * 4) + { + FileUploading++; + StatusMessage = string.Format(UploadingFilesMessageTemplate, FileUploading); + VisualStateManager.GoToState(this, UploadStatusUploading, false); + try + { + await GraphService.TryLoginAsync(); + GraphServiceClient graphServiceClient = GraphService.GraphProvider; + await graphServiceClient.Drives[_driveId].Items[driveItemId].ItemWithPath(file.Name).Content.Request().PutAsync(inputStream, _cancelUpload.Token); + VisualStateManager.GoToState(this, UploadStatusNotUploading, false); + FileUploading--; + } + catch (Exception ex) + { + FileUploading--; + ErrorMessage = ex.Message; + VisualStateManager.GoToState(this, UploadStatusError, false); + } + + await LoadFilesAsync(driveItemId); + } + } + } + } + + private async void Share_Click(object sender, RoutedEventArgs e) + { + if (_list.SelectedItem is DriveItem driveItem) + { + try + { + await GraphService.TryLoginAsync(); + GraphServiceClient graphServiceClient = GraphService.GraphProvider; + Permission link = await graphServiceClient.Drives[_driveId].Items[driveItem.Id].CreateLink("view", "organization").Request().PostAsync(); + MessageDialog dialog = new MessageDialog(link.Link.WebUrl, ShareLinkCopiedMessage); + DataPackage package = new DataPackage(); + package.SetText(link.Link.WebUrl); + Clipboard.SetContent(package); + await dialog.ShowAsync(); + } + catch (Exception exception) + { + MessageDialog dialog = new MessageDialog(exception.Message); + await dialog.ShowAsync(); + } + } + } + + private async void Download_Click(object sender, RoutedEventArgs e) + { + if (_list.SelectedItem is DriveItem driveItem) + { + await GraphService.TryLoginAsync(); + GraphServiceClient graphServiceClient = GraphService.GraphProvider; + FileSavePicker picker = new FileSavePicker(); + picker.FileTypeChoices.Add(AllFilesMessage, new List() { driveItem.Name.Substring(driveItem.Name.LastIndexOf(".")) }); + picker.SuggestedFileName = driveItem.Name; + picker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary; + StorageFile file = await picker.PickSaveFileAsync(); + if (file != null) + { + using (Stream inputStream = await graphServiceClient.Drives[_driveId].Items[driveItem.Id].Content.Request().GetAsync()) + { + using (Stream outputStream = await file.OpenStreamForWriteAsync()) + { + await inputStream.CopyToAsync(outputStream); + } + } + } + } + } + + private async void Delete_Click(object sender, RoutedEventArgs e) + { + if (_list.SelectedItem is DriveItem driveItem) + { + MessageDialog confirmDialog = new MessageDialog(DeleteConfirmMessage); + confirmDialog.Commands.Add(new UICommand(DeleteConfirmOkMessage, cmd => { }, commandId: 0)); + confirmDialog.Commands.Add(new UICommand(DeleteConfirmCancelMessage, cmd => { }, commandId: 1)); + + confirmDialog.DefaultCommandIndex = 0; + confirmDialog.CancelCommandIndex = 1; + + IUICommand result = await confirmDialog.ShowAsync(); + + if ((int)result.Id == 0) + { + await GraphService.TryLoginAsync(); + GraphServiceClient graphServiceClient = GraphService.GraphProvider; + await graphServiceClient.Drives[_driveId].Items[driveItem.Id].Request().DeleteAsync(); + string driveItemId = _driveItemPath.Peek()?.Id; + await LoadFilesAsync(driveItemId); + } + } + } + + private async void ShowErrorDetails_Click(object sender, RoutedEventArgs e) + { + MessageDialog messageDialog = new MessageDialog(ErrorMessage); + await messageDialog.ShowAsync(); + } + + private async void LoadMore_Click(object sender, RoutedEventArgs e) + { + await LoadNextPageAsync(); + } + + private void Cancel_Click(object sender, RoutedEventArgs e) + { + _cancelUpload.Cancel(false); + _cancelUpload.Dispose(); + _cancelUpload = new CancellationTokenSource(); + } + + private async void List_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + _cancelGetDetails.Cancel(false); + _cancelGetDetails.Dispose(); + _cancelGetDetails = new CancellationTokenSource(); + if (_list.SelectedItem is DriveItem driveItem && driveItem.File != null) + { + try + { + SelectedFile = driveItem; + FileSize = driveItem.Size ?? 0; + LastModified = driveItem.LastModifiedDateTime?.LocalDateTime.ToString() ?? string.Empty; + if (FileSelected != null) + { + FileSelected.Invoke(this, new FileSelectedEventArgs(driveItem)); + } + + ThumbnailImageSource = null; + VisualStateManager.GoToState(this, NavStatesFileReadonly, false); + await GraphService.TryLoginAsync(); + GraphServiceClient graphServiceClient = GraphService.GraphProvider; + Task taskPermissions = graphServiceClient.Drives[_driveId].Items[driveItem.Id].Permissions.Request().GetAsync(_cancelGetDetails.Token); + IDriveItemPermissionsCollectionPage permissions = await taskPermissions; + if (!taskPermissions.IsCanceled) + { + foreach (Permission permission in permissions) + { + if (permission.Roles.Contains("write") || permission.Roles.Contains("owner")) + { + VisualStateManager.GoToState(this, NavStatesFileEdit, false); + break; + } + } + + Task taskThumbnails = graphServiceClient.Drives[_driveId].Items[driveItem.Id].Thumbnails.Request().GetAsync(_cancelGetDetails.Token); + IDriveItemThumbnailsCollectionPage thumbnails = await taskThumbnails; + if (!taskThumbnails.IsCanceled) + { + ThumbnailSet thumbnailsSet = thumbnails.FirstOrDefault(); + if (thumbnailsSet != null) + { + Thumbnail thumbnail = thumbnailsSet.Large; + if (thumbnail.Url.Contains("inputFormat=svg")) + { + SvgImageSource source = new SvgImageSource(); + using (Stream inputStream = await graphServiceClient.Drives[_driveId].Items[driveItem.Id].Content.Request().GetAsync()) + { + SvgImageSourceLoadStatus status = await source.SetSourceAsync(inputStream.AsRandomAccessStream()); + if (status == SvgImageSourceLoadStatus.Success) + { + ThumbnailImageSource = source; + } + } + } + else + { + ThumbnailImageSource = new BitmapImage(new Uri(thumbnail.Url)); + } + } + } + + IsDetailPaneVisible = true; + ShowDetailsPane(); + } + } + catch + { + } + } + else + { + SelectedFile = null; + FileSize = 0; + LastModified = string.Empty; + VisualStateManager.GoToState(this, _pathVisualState, false); + IsDetailPaneVisible = false; + HideDetailsPane(); + } + } + + private async void List_ItemClick(object sender, ItemClickEventArgs e) + { + if (e.ClickedItem is DriveItem driveItem && driveItem.Folder != null) + { + _driveItemPath.Push(driveItem); + UpdateCurrentPath(); + BackButtonVisibility = Visibility.Visible; + await LoadFilesAsync(driveItem.Id); + } + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/SharePointFileList.Properties.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/SharePointFileList.Properties.cs new file mode 100644 index 00000000000..478aa2f014e --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/SharePointFileList.Properties.cs @@ -0,0 +1,340 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE 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 CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +using Microsoft.Graph; +using Microsoft.Toolkit.Services.MicrosoftGraph; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Media; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Graph +{ + /// + /// The SharePointFiles Control displays a simple list of SharePoint Files. + /// + public partial class SharePointFileList + { + /// + /// Gets the instance + /// + public static MicrosoftGraphService GraphService => MicrosoftGraphService.Instance; + + /// + /// Gets required delegated permissions for the control + /// + public static string[] RequiredDelegatedPermissions + { + get + { + return new string[] { "User.Read", "Files.ReadWrite.All" }; + } + } + + /// + /// Url of OneDrive to display + /// + public static readonly DependencyProperty DriveUrlProperty = + DependencyProperty.Register( + nameof(DriveUrl), + typeof(string), + typeof(SharePointFileList), + new PropertyMetadata(string.Empty, DriveUrlPropertyChanged)); + + /// + /// How details of a file shows + /// + public static readonly DependencyProperty DetailPaneProperty = + DependencyProperty.Register( + nameof(DetailPane), + typeof(DetailPaneDisplayMode), + typeof(SharePointFileList), + new PropertyMetadata(DetailPaneDisplayMode.Disabled, DetailPanePropertyChanged)); + + /// + /// Page size of each request + /// + public static readonly DependencyProperty PageSizeProperty = + DependencyProperty.Register( + nameof(PageSize), + typeof(int), + typeof(SharePointFileList), + new PropertyMetadata(20)); + + /// + /// Share link copied message + /// + public static readonly DependencyProperty ShareLinkCopiedMessageProperty = + DependencyProperty.Register( + nameof(ShareLinkCopiedMessage), + typeof(string), + typeof(SharePointFileList), + new PropertyMetadata("Link copied!")); + + /// + /// All files message + /// + public static readonly DependencyProperty AllFilesMessageProperty = + DependencyProperty.Register( + nameof(AllFilesMessage), + typeof(string), + typeof(SharePointFileList), + new PropertyMetadata("All Files")); + + /// + /// Delete confirm message + /// + public static readonly DependencyProperty DeleteConfirmMessageProperty = + DependencyProperty.Register( + nameof(DeleteConfirmMessage), + typeof(string), + typeof(SharePointFileList), + new PropertyMetadata("Do you want to delete this file?")); + + /// + /// Delete confirm Ok message + /// + public static readonly DependencyProperty DeleteConfirmOkMessageProperty = + DependencyProperty.Register( + nameof(DeleteConfirmOkMessage), + typeof(string), + typeof(SharePointFileList), + new PropertyMetadata("OK")); + + /// + /// Delete confirm cancel message + /// + public static readonly DependencyProperty DeleteConfirmCancelMessageProperty = + DependencyProperty.Register( + nameof(DeleteConfirmCancelMessage), + typeof(string), + typeof(SharePointFileList), + new PropertyMetadata("Cancel")); + + /// + /// Uploading files message template + /// + public static readonly DependencyProperty UploadingFilesMessageTemplateProperty = + DependencyProperty.Register( + nameof(UploadingFilesMessageTemplate), + typeof(string), + typeof(SharePointFileList), + new PropertyMetadata("Uploading {0} files...")); + + internal static readonly DependencyProperty ThumbnailImageSourceProperty = + DependencyProperty.Register( + nameof(ThumbnailImageSource), + typeof(ImageSource), + typeof(SharePointFileList), + new PropertyMetadata(null)); + + internal static readonly DependencyProperty HasMoreProperty = + DependencyProperty.Register( + nameof(HasMore), + typeof(bool), + typeof(SharePointFileList), + new PropertyMetadata(false)); + + internal static readonly DependencyProperty SelectedFileProperty = + DependencyProperty.Register( + nameof(SelectedFile), + typeof(DriveItem), + typeof(SharePointFileList), + new PropertyMetadata(null)); + + internal static readonly DependencyProperty SizeProperty = + DependencyProperty.Register( + nameof(FileSize), + typeof(long), + typeof(SharePointFileList), + new PropertyMetadata(0L)); + + internal static readonly DependencyProperty LastModifiedProperty = + DependencyProperty.Register( + nameof(LastModified), + typeof(string), + typeof(SharePointFileList), + null); + + internal static readonly DependencyProperty BackButtonVisibilityProperty = + DependencyProperty.Register( + nameof(BackButtonVisibility), + typeof(Visibility), + typeof(SharePointFileList), + new PropertyMetadata(Visibility.Collapsed)); + + internal static readonly DependencyProperty StatusMessageProperty = + DependencyProperty.Register( + nameof(StatusMessage), + typeof(string), + typeof(SharePointFileList), + new PropertyMetadata(string.Empty)); + + private static readonly DependencyProperty IsDetailPaneVisibleProperty = + DependencyProperty.Register( + nameof(IsDetailPaneVisible), + typeof(bool), + typeof(SharePointFileList), + new PropertyMetadata(false)); + + internal static readonly DependencyProperty CurrentPathProperty = + DependencyProperty.Register( + nameof(CurrentPath), + typeof(string), + typeof(SharePointFileList), + new PropertyMetadata(string.Empty)); + + /// + /// Gets or sets drive or SharePoint document library URL to display + /// + public string DriveUrl + { + get { return ((string)GetValue(DriveUrlProperty))?.Trim(); } + set { SetValue(DriveUrlProperty, value?.Trim()); } + } + + /// + /// Gets or sets how DetailPane shows + /// + public DetailPaneDisplayMode DetailPane + { + get { return (DetailPaneDisplayMode)GetValue(DetailPaneProperty); } + set { SetValue(DetailPaneProperty, value); } + } + + /// + /// Gets or sets page size of each request + /// + public int PageSize + { + get { return (int)GetValue(PageSizeProperty); } + set { SetValue(PageSizeProperty, value); } + } + + /// + /// Gets or sets the message when share link copied + /// + public string ShareLinkCopiedMessage + { + get { return (string)GetValue(ShareLinkCopiedMessageProperty); } + set { SetValue(ShareLinkCopiedMessageProperty, value); } + } + + /// + /// Gets or sets the label of All Files + /// + public string AllFilesMessage + { + get { return (string)GetValue(AllFilesMessageProperty); } + set { SetValue(AllFilesMessageProperty, value); } + } + + /// + /// Gets or sets the message of delete confirm dialog + /// + public string DeleteConfirmMessage + { + get { return (string)GetValue(DeleteConfirmMessageProperty); } + set { SetValue(DeleteConfirmMessageProperty, value); } + } + + /// + /// Gets or sets the caption of ok button in delete confirm dialog + /// + public string DeleteConfirmOkMessage + { + get { return (string)GetValue(DeleteConfirmOkMessageProperty); } + set { SetValue(DeleteConfirmOkMessageProperty, value); } + } + + /// + /// Gets or sets the caption of cancel button in delete confirm dialog + /// + public string DeleteConfirmCancelMessage + { + get { return (string)GetValue(DeleteConfirmCancelMessageProperty); } + set { SetValue(DeleteConfirmCancelMessageProperty, value); } + } + + /// + /// Gets or sets the template of uploading files + /// + public string UploadingFilesMessageTemplate + { + get { return (string)GetValue(UploadingFilesMessageTemplateProperty); } + set { SetValue(UploadingFilesMessageTemplateProperty, value); } + } + + internal bool HasMore + { + get { return (bool)GetValue(HasMoreProperty); } + set { SetValue(HasMoreProperty, value); } + } + + internal DriveItem SelectedFile + { + get { return (DriveItem)GetValue(SelectedFileProperty); } + set { SetValue(SelectedFileProperty, value); } + } + + internal long FileSize + { + get { return (long)GetValue(SizeProperty); } + set { SetValue(SizeProperty, value); } + } + + internal string LastModified + { + get { return (string)GetValue(LastModifiedProperty); } + set { SetValue(LastModifiedProperty, value); } + } + + internal ImageSource ThumbnailImageSource + { + get { return (ImageSource)GetValue(ThumbnailImageSourceProperty); } + set { SetValue(ThumbnailImageSourceProperty, value); } + } + + internal string StatusMessage + { + get { return (string)GetValue(StatusMessageProperty); } + set { SetValue(StatusMessageProperty, value); } + } + + internal Visibility BackButtonVisibility + { + get { return (Visibility)GetValue(BackButtonVisibilityProperty); } + set { SetValue(BackButtonVisibilityProperty, value); } + } + + internal string CurrentPath + { + get { return (string)GetValue(CurrentPathProperty); } + set { SetValue(CurrentPathProperty, value); } + } + + private bool IsDetailPaneVisible + { + get + { + return (bool)GetValue(IsDetailPaneVisibleProperty); + } + + set + { + SetValue(IsDetailPaneVisibleProperty, value); + } + } + + private int FileUploading { get; set; } + + private string ErrorMessage { get; set; } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/SharePointFileList.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/SharePointFileList.cs new file mode 100644 index 00000000000..afaded690b8 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Graph/SharePointFileList/SharePointFileList.cs @@ -0,0 +1,349 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE 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 CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Graph; +using Newtonsoft.Json; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Graph +{ + /// + /// The SharePointFiles Control displays a simple list of SharePoint Files. + /// + [TemplatePart(Name = ControlFileList, Type = typeof(ListView))] + [TemplatePart(Name = ControlBack, Type = typeof(Button))] + [TemplatePart(Name = ControlCancel, Type = typeof(Button))] + [TemplatePart(Name = ControlDelete, Type = typeof(Button))] + [TemplatePart(Name = ControlDownload, Type = typeof(Button))] + [TemplatePart(Name = ControlLoadMore, Type = typeof(Button))] + [TemplatePart(Name = ControlShare, Type = typeof(Button))] + [TemplatePart(Name = ControlUpload, Type = typeof(Button))] + [TemplatePart(Name = ControlError, Type = typeof(HyperlinkButton))] + [TemplateVisualState(Name = UploadStatusNotUploading, GroupName = UploadStatus)] + [TemplateVisualState(Name = UploadStatusUploading, GroupName = UploadStatus)] + [TemplateVisualState(Name = UploadStatusError, GroupName = UploadStatus)] + [TemplateVisualState(Name = DetailPaneStatesHide, GroupName = DetailPaneStates)] + [TemplateVisualState(Name = DetailPaneStatesSide, GroupName = DetailPaneStates)] + [TemplateVisualState(Name = DetailPaneStatesBottom, GroupName = DetailPaneStates)] + [TemplateVisualState(Name = DetailPaneStatesFull, GroupName = DetailPaneStates)] + [TemplateVisualState(Name = NavStatesFolderReadonly, GroupName = NavStates)] + [TemplateVisualState(Name = NavStatesFolderEdit, GroupName = NavStates)] + [TemplateVisualState(Name = NavStatesFileReadonly, GroupName = NavStates)] + [TemplateVisualState(Name = NavStatesFileEdit, GroupName = NavStates)] + public partial class SharePointFileList : Control + { + /// + /// File is selected + /// + public event EventHandler FileSelected; + + private string _driveId; + private string _driveName; + private Stack _driveItemPath = new Stack(); + private string _pathVisualState = string.Empty; + private IDriveItemChildrenCollectionRequest _nextPageRequest = null; + private CancellationTokenSource _cancelUpload = new CancellationTokenSource(); + private CancellationTokenSource _cancelLoadFile = new CancellationTokenSource(); + private CancellationTokenSource _cancelGetDetails = new CancellationTokenSource(); + private ListView _list; + + /// + /// Initializes a new instance of the class. + /// + public SharePointFileList() + { + DefaultStyleKey = typeof(SharePointFileList); + } + + /// + /// Called when applying the control template. + /// + protected override void OnApplyTemplate() + { + _list = GetTemplateChild(ControlFileList) as ListView; + if (_list != null) + { + _list.SelectionChanged += List_SelectionChanged; + _list.ItemClick += List_ItemClick; + } + + Button back = GetTemplateChild(ControlBack) as Button; + if (back != null) + { + back.Click += Back_Click; + } + + Button cancel = GetTemplateChild(ControlCancel) as Button; + if (cancel != null) + { + cancel.Click += Cancel_Click; + } + + Button delete = GetTemplateChild(ControlDelete) as Button; + if (delete != null) + { + delete.Click += Delete_Click; + } + + Button download = GetTemplateChild(ControlDownload) as Button; + if (download != null) + { + download.Click += Download_Click; + } + + Button loadMore = GetTemplateChild(ControlLoadMore) as Button; + if (loadMore != null) + { + loadMore.Click += LoadMore_Click; + } + + Button share = GetTemplateChild(ControlShare) as Button; + if (share != null) + { + share.Click += Share_Click; + } + + Button upload = GetTemplateChild(ControlUpload) as Button; + if (upload != null) + { + upload.Click += Upload_Click; + } + + HyperlinkButton error = GetTemplateChild(ControlError) as HyperlinkButton; + if (error != null) + { + error.Click += ShowErrorDetails_Click; + } + + base.OnApplyTemplate(); + } + + /// + /// Retrieves an appropriate Drive URL from a SharePoint document library root URL + /// + /// Raw URL for SharePoint document library + /// Drive URL + public async Task GetDriveUrlFromSharePointUrlAsync(string rawDocLibUrl) + { + if (string.IsNullOrEmpty(rawDocLibUrl)) + { + return rawDocLibUrl; + } + + rawDocLibUrl = WebUtility.UrlDecode(rawDocLibUrl); + + Match match = Regex.Match(rawDocLibUrl, @"(https?://([^/]+)((/[^/?]+)*?)(/[^/?]+))(/(Forms/\w+.aspx)?)?(\?.*)?$", RegexOptions.IgnoreCase); + string docLibUrl = match.Groups[1].Value; + string hostName = match.Groups[2].Value; + string siteRelativePath = match.Groups[3].Value; + if (string.IsNullOrEmpty(siteRelativePath)) + { + siteRelativePath = "/"; + } + + if (await GraphService.TryLoginAsync()) + { + GraphServiceClient graphServiceClient = GraphService.GraphProvider; + + Site site = await graphServiceClient.Sites.GetByPath(siteRelativePath, hostName).Request().GetAsync(); + ISiteDrivesCollectionPage drives = await graphServiceClient.Sites[site.Id].Drives.Request().GetAsync(); + + Drive drive = drives.SingleOrDefault(o => WebUtility.UrlDecode(o.WebUrl).Equals(docLibUrl, StringComparison.CurrentCultureIgnoreCase)); + if (drive == null) + { + throw new Exception("Drive not found"); + } + + return graphServiceClient.Drives[drive.Id].RequestUrl; + } + + return rawDocLibUrl; + } + + private async Task InitDriveAsync(string driveUrl) + { + try + { + string realDriveURL; + if (driveUrl.StartsWith("https://graph.microsoft.com/", StringComparison.CurrentCultureIgnoreCase)) + { + realDriveURL = driveUrl; + } + else + { + realDriveURL = await GetDriveUrlFromSharePointUrlAsync(driveUrl); + } + + await GraphService.TryLoginAsync(); + GraphServiceClient graphServiceClient = GraphService.GraphProvider; + HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Get, realDriveURL); + await graphServiceClient.AuthenticationProvider.AuthenticateRequestAsync(message); + + HttpResponseMessage result = await graphServiceClient.HttpProvider.SendAsync(message); + if (result.StatusCode == HttpStatusCode.OK) + { + string json = await result.Content.ReadAsStringAsync(); + Drive drive = JsonConvert.DeserializeObject(json); + if (drive != null) + { + _driveId = drive.Id; + _driveName = drive.Name; + _driveItemPath.Clear(); + DriveItem rootDriveItem = await graphServiceClient.Drives[_driveId].Root.Request().GetAsync(); + _driveItemPath.Push(rootDriveItem); + UpdateCurrentPath(); + await LoadFilesAsync(rootDriveItem.Id); + BackButtonVisibility = Visibility.Collapsed; + } + } + } + catch (Exception) + { + } + } + + private async Task LoadFilesAsync(string driveItemId, int pageIndex = 0) + { + IsDetailPaneVisible = false; + HideDetailsPane(); + if (!string.IsNullOrEmpty(_driveId)) + { + try + { + _cancelLoadFile.Cancel(false); + _cancelLoadFile.Dispose(); + _cancelLoadFile = new CancellationTokenSource(); + _list.Items.Clear(); + VisualStateManager.GoToState(this, NavStatesFolderReadonly, false); + QueryOption queryOption = new QueryOption("$top", PageSize.ToString()); + + await GraphService.TryLoginAsync(); + GraphServiceClient graphServiceClient = GraphService.GraphProvider; + Task taskFiles = graphServiceClient.Drives[_driveId].Items[driveItemId].Children.Request(new List