Dynamics 365 SDK/Tools/WebResourceUtility 是一个非常实用的工具,
在开发WebResource脚本时,常常需要把本地开发上传到开发环境,实用此工具不需要打开CRM找到具体的WebResource上传然后再发布,
利用此工具可以批量上传本地代码到开发环境并自动发布已上传的代码。
但工具包本身包含一些问题,幸好工具包提供源码,我修复了部分问题,支持OnPremise / IFD / Online Office365部署。
Update point:
- only publish WebResources which already uploaded.
- fixed authentication failed issue.
- support office 365 authenticate.
- online federation authenticate has been commented.
ConsolelessServerConnection.cs
using System; using System.Collections.Generic; using System.ServiceModel.Description; using System.Linq; // These namespaces are found in the Microsoft.Xrm.Sdk.dll assembly // located in the SDKin folder of the SDK download. using Microsoft.Xrm.Sdk.Client; using Microsoft.Xrm.Sdk.Discovery; using Microsoft.Xrm.Sdk; using System.DirectoryServices.AccountManagement; namespace Microsoft.Crm.Sdk.Samples { public class ConsolelessServerConnection : ServerConnection { #region Private properties private Configuration config = new Configuration(); #endregion Private properties public virtual ServerConnection.Configuration GetServerConfiguration(string server, string orgName, string user, string pw, string domain ) { config.ServerAddress = server; if (config.ServerAddress.EndsWith(".dynamics.com")) { config.EndpointType = AuthenticationProviderType.LiveId; config.DiscoveryUri = new Uri(String.Format("https://disco.{0}/XRMServices/2011/Discovery.svc", config.ServerAddress.Replace(orgName+".",""))); config.DeviceCredentials = GetDeviceCredentials(); ClientCredentials credentials = new ClientCredentials(); credentials.UserName.UserName = user; credentials.UserName.Password = pw; config.Credentials = credentials; config.OrganizationUri = GetOrganizationAddress(config.DiscoveryUri, orgName); } else if (config.ServerAddress.EndsWith(".com")) { config.EndpointType = AuthenticationProviderType.Federation; config.DiscoveryUri = new Uri(String.Format("https://{0}/XRMServices/2011/Discovery.svc", config.ServerAddress)); ClientCredentials credentials = new ClientCredentials(); credentials.Windows.ClientCredential = new System.Net.NetworkCredential(user, pw, domain); config.Credentials = credentials; config.OrganizationUri = GetOrganizationAddress(config.DiscoveryUri, orgName); } else { config.EndpointType = AuthenticationProviderType.ActiveDirectory; config.DiscoveryUri = new Uri(String.Format("http://{0}/XRMServices/2011/Discovery.svc", config.ServerAddress)); ClientCredentials credentials = new ClientCredentials(); credentials.Windows.ClientCredential = new System.Net.NetworkCredential(user, pw, domain); config.Credentials = credentials; config.OrganizationUri = GetOrganizationAddress(config.DiscoveryUri, orgName); } if (configurations == null) configurations = new List<Configuration>(); configurations.Add(config); return config; } public DiscoveryServiceProxy GetDiscoveryServiceProxy(Uri discoveryServiceUri) { IServiceManagement<IDiscoveryService> serviceManagement = ServiceConfigurationFactory.CreateManagement<IDiscoveryService>( discoveryServiceUri); AuthenticationProviderType endpointType = serviceManagement.AuthenticationType; AuthenticationCredentials authCredentials = GetCredentials(serviceManagement, endpointType); String organizationUri = String.Empty; return GetProxy<IDiscoveryService, DiscoveryServiceProxy>(serviceManagement, authCredentials); } public OrganizationServiceProxy GetOrganizationServiceProxy(Uri discoveryServiceUri, string orgName) { IServiceManagement<IDiscoveryService> serviceManagement = ServiceConfigurationFactory.CreateManagement<IDiscoveryService>( discoveryServiceUri); AuthenticationProviderType endpointType = serviceManagement.AuthenticationType; AuthenticationCredentials authCredentials = GetCredentials(serviceManagement, endpointType); String organizationUri = String.Empty; // Get the discovery service proxy. using (DiscoveryServiceProxy discoveryProxy = GetProxy<IDiscoveryService, DiscoveryServiceProxy>(serviceManagement, authCredentials)) { if (discoveryProxy != null) { // Obtain information about the organizations that the system user belongs to. OrganizationDetailCollection orgs = DiscoverOrganizations(discoveryProxy); OrganizationDetail org = orgs.Where(x => x.UniqueName.ToLower() == orgName.ToLower()).FirstOrDefault(); if (org != null) { Uri orgUri = new System.Uri(org.Endpoints[EndpointType.OrganizationService]); IServiceManagement<IOrganizationService> orgServiceManagement = ServiceConfigurationFactory.CreateManagement<IOrganizationService>(orgUri); // Set the credentials. AuthenticationCredentials credentials = GetCredentials(orgServiceManagement, endpointType); return GetProxy<IOrganizationService, OrganizationServiceProxy>(orgServiceManagement, credentials); } else { throw new InvalidOperationException("That OrgName does not exist on that server."); } } else throw new Exception("An invalid server name was specified."); } } protected virtual Uri GetOrganizationAddress(Uri discoveryServiceUri, string orgName) { IServiceManagement<IDiscoveryService> serviceManagement = ServiceConfigurationFactory.CreateManagement<IDiscoveryService>( discoveryServiceUri); AuthenticationProviderType endpointType = serviceManagement.AuthenticationType; AuthenticationCredentials authCredentials = GetCredentials(serviceManagement, endpointType); String organizationUri = String.Empty; // Get the discovery service proxy. using (DiscoveryServiceProxy discoveryProxy = GetProxy<IDiscoveryService, DiscoveryServiceProxy>(serviceManagement, authCredentials)) { // Obtain organization information from the Discovery service. if (discoveryProxy != null) { // Obtain information about the organizations that the system user belongs to. OrganizationDetailCollection orgs = DiscoverOrganizations(discoveryProxy); OrganizationDetail org = orgs.Where(x => x.UrlName.ToLower() == orgName.ToLower()).FirstOrDefault(); if (org != null) { return new System.Uri(org.Endpoints[EndpointType.OrganizationService]); } else { throw new InvalidOperationException("That OrgName does not exist on that server."); } } else throw new Exception("An invalid server name was specified."); } } #region CustomFunctions 修改获取发现服务或组织服务代码 public OrganizationServiceProxy GetOrganizationServiceProxy(Uri orgUri, AuthenticationProviderType endpointType) { IServiceManagement<IOrganizationService> orgServiceManagement = ServiceConfigurationFactory.CreateManagement<IOrganizationService>(orgUri); // Set the credentials. AuthenticationCredentials credentials = GetCredentials(orgServiceManagement, endpointType); return GetProxy<IOrganizationService, OrganizationServiceProxy>(orgServiceManagement, credentials); } /// <summary> /// Generic method to obtain discovery/organization service proxy instance. /// </summary> /// <typeparam name="TService"> /// Set IDiscoveryService or IOrganizationService type to request respective service proxy instance. /// </typeparam> /// <typeparam name="TProxy"> /// Set the return type to either DiscoveryServiceProxy or OrganizationServiceProxy type based on TService type. /// </typeparam> /// <param name="serviceManagement">An instance of IServiceManagement</param> /// <param name="authCredentials">The user's Microsoft Dynamics CRM logon credentials.</param> /// <returns></returns> /// <snippetAuthenticateWithNoHelp4> public TProxy GetProxy<TService, TProxy>( IServiceManagement<TService> serviceManagement, AuthenticationCredentials authCredentials) where TService : class where TProxy : ServiceProxy<TService> { Type classType = typeof(TProxy); if (serviceManagement.AuthenticationType != AuthenticationProviderType.ActiveDirectory) { AuthenticationCredentials tokenCredentials = serviceManagement.Authenticate(authCredentials); // Obtain discovery/organization service proxy for Federated, LiveId and OnlineFederated environments. // Instantiate a new class of type using the 2 parameter constructor of type IServiceManagement and SecurityTokenResponse. return (TProxy)classType .GetConstructor(new Type[] { typeof(IServiceManagement<TService>), typeof(SecurityTokenResponse) }) .Invoke(new object[] { serviceManagement, tokenCredentials.SecurityTokenResponse }); } // Obtain discovery/organization service proxy for ActiveDirectory environment. // Instantiate a new class of type using the 2 parameter constructor of type IServiceManagement and ClientCredentials. return (TProxy)classType .GetConstructor(new Type[] { typeof(IServiceManagement<TService>), typeof(ClientCredentials) }) .Invoke(new object[] { serviceManagement, authCredentials.ClientCredentials }); } //<snippetAuthenticateWithNoHelp2> /// <summary> /// Obtain the AuthenticationCredentials based on AuthenticationProviderType. /// </summary> /// <param name="service">A service management object.</param> /// <param name="endpointType">An AuthenticationProviderType of the CRM environment.</param> /// <returns>Get filled credentials.</returns> public AuthenticationCredentials GetCredentials<TService>(IServiceManagement<TService> service, AuthenticationProviderType endpointType) { AuthenticationCredentials authCredentials = new AuthenticationCredentials(); switch (endpointType) { case AuthenticationProviderType.ActiveDirectory: authCredentials.ClientCredentials.Windows.ClientCredential = new System.Net.NetworkCredential(config.Credentials.UserName.UserName, config.Credentials.UserName.Password, config.Credentials.Windows.ClientCredential.Domain); break; case AuthenticationProviderType.LiveId: authCredentials.ClientCredentials.UserName.UserName = config.Credentials.UserName.UserName; authCredentials.ClientCredentials.UserName.Password = config.Credentials.UserName.Password; authCredentials.SupportingCredentials = new AuthenticationCredentials(); authCredentials.SupportingCredentials.ClientCredentials = Microsoft.Crm.Services.Utility.DeviceIdManager.LoadOrRegisterDevice(); break; default: // For Federated and OnlineFederated environments. authCredentials.ClientCredentials.UserName.UserName = config.Credentials.UserName.UserName; authCredentials.ClientCredentials.UserName.Password = config.Credentials.UserName.Password; // For OnlineFederated single-sign on, you could just use current UserPrincipalName instead of passing user name and password. //authCredentials.UserPrincipalName = UserPrincipal.Current.UserPrincipalName; // Windows Kerberos //The service is configured for User Id authentication, but the user might provide Microsoft //account credentials.If so, the supporting credentials must contain the device credentials. if (endpointType == AuthenticationProviderType.OnlineFederation) { IdentityProvider provider = service.GetIdentityProvider(authCredentials.UserPrincipalName); if (provider != null && provider.IdentityProviderType == IdentityProviderType.LiveId) { authCredentials.SupportingCredentials = new AuthenticationCredentials(); authCredentials.SupportingCredentials.ClientCredentials = Microsoft.Crm.Services.Utility.DeviceIdManager.LoadOrRegisterDevice(); } } break; } return authCredentials; } #endregion } }
MainWindowViewModel.cs
using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Text; using System.Windows; using System.Windows.Data; using System.Windows.Input; using System.Xml.Linq; using Microsoft.Crm.Sdk.Messages; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Client; using Microsoft.Xrm.Sdk.Query; using WebResourceUtility.Model; using System.Text.RegularExpressions; using System.ComponentModel; using System.Windows.Threading; namespace Microsoft.Crm.Sdk.Samples { public class MainWindowViewModel : ViewModelBase { #region Commands RelayCommand _hideOutputWindow; public ICommand HideOutputWindow { get { if (_hideOutputWindow == null) { _hideOutputWindow = new RelayCommand( param => { IsOutputWindowDisplayed = false; }); } return _hideOutputWindow; } } RelayCommand _showOutputWindow; public ICommand ShowOutputWindow { get { if (_showOutputWindow == null) { _showOutputWindow = new RelayCommand( param => { IsOutputWindowDisplayed = true; }); } return _showOutputWindow; } } RelayCommand _browseFolderCommand; public ICommand BrowseFolderCommand { get { if (_browseFolderCommand == null) { _browseFolderCommand = new RelayCommand( param => this.ShowBrowseFolderDialog()); } return _browseFolderCommand; } } RelayCommand _activateConnectionCommand; public ICommand ActivateConnectionCommand { get { if (_activateConnectionCommand == null) { _activateConnectionCommand = new RelayCommand( param => this.ActivateSelectedConfiguration(), param => this.CanActivateSelectedConfiguration()); } return _activateConnectionCommand; } } RelayCommand _createNewConnectionCommand; public ICommand CreateNewConnectionCommand { get { if (_createNewConnectionCommand == null) { _createNewConnectionCommand = new RelayCommand( param => this.CreateNewConfiguration()); } return _createNewConnectionCommand; } } RelayCommand _deleteConnectionCommand; public ICommand DeleteConnectionCommand { get { if (_deleteConnectionCommand == null) { _deleteConnectionCommand = new RelayCommand( param => this.DeleteSelectedConfiguration()); } return _deleteConnectionCommand; } } RelayCommand _activateSolutionCommand; public ICommand ActivateSolutionCommand { get { if (_activateSolutionCommand == null) { _activateSolutionCommand = new RelayCommand( param => this.ActivateSelectedSolution()); } return _activateSolutionCommand; } } RelayCommand _activateSelectedPackageCommand; public ICommand ActivateSelectedPackageCommand { get { if (_activateSelectedPackageCommand == null) { _activateSelectedPackageCommand = new RelayCommand( param => this.ActivatePackage()); } return _activateSelectedPackageCommand; } } RelayCommand _createNewPackageCommand; public ICommand CreateNewPackageCommand { get { if (_createNewPackageCommand == null) { _createNewPackageCommand = new RelayCommand( param => this.CreateNewPackage()); } return _createNewPackageCommand; } } RelayCommand _deleteActivePackageCommand; public ICommand DeleteActivePackageCommand { get { if (_deleteActivePackageCommand == null) { _deleteActivePackageCommand = new RelayCommand( param => this.DeleteSelectedPackage()); } return _deleteActivePackageCommand; } } RelayCommand _saveActivePackageCommand; public ICommand SaveActivePackageCommand { get { if (_saveActivePackageCommand == null) { _saveActivePackageCommand = new RelayCommand( param => SavePackages()); } return _saveActivePackageCommand; } } RelayCommand _refreshFilesCommand; public ICommand RefreshFilesCommand { get { if (_refreshFilesCommand == null) { _refreshFilesCommand = new RelayCommand( param => this.SearchAndPopulateFiles()); } return _refreshFilesCommand; } } RelayCommand _saveConnectionsCommand; public ICommand SaveConnectionsCommand { get { if (_saveConnectionsCommand == null) { _saveConnectionsCommand = new RelayCommand( param => SaveConfigurations()); } return _saveConnectionsCommand; } } RelayCommand<IEnumerable> _convertFileToResourceCommand; public ICommand ConvertFileToResourceCommand { get { if (_convertFileToResourceCommand == null) { _convertFileToResourceCommand = new RelayCommand<IEnumerable>(AddFilesToWebResources); } return _convertFileToResourceCommand; } } RelayCommand<IEnumerable> _uploadWebResourcesCommand; public ICommand UploadWebResourcesCommand { get { if (_uploadWebResourcesCommand == null) { _uploadWebResourcesCommand = new RelayCommand<IEnumerable>(UploadWebResources, param => CanUploadWebResource()); } return _uploadWebResourcesCommand; } } RelayCommand _uploadAllWebResourcesCommand; public ICommand UploadAllWebResourcesCommand { get { if (_uploadAllWebResourcesCommand == null) { _uploadAllWebResourcesCommand = new RelayCommand(param => UploadAllWebResources(), param => CanUploadWebResource()); } return _uploadAllWebResourcesCommand; } } RelayCommand<IEnumerable> _deleteWebResourcesCommand; public ICommand DeleteWebResourcesCommand { get { if (_deleteWebResourcesCommand == null) { _deleteWebResourcesCommand = new RelayCommand<IEnumerable>(DeleteSelectedWebResources); } return _deleteWebResourcesCommand; } } #endregion #region Properties public const string CONFIG_FILENAME = @"configurations.xml"; public const string PACKAGES_FILENAME = @"packages.xml"; public const string VALID_NAME_MSG = "ERROR: Web Resource names cannot contain spaces or hyphens. They must be alphanumeric and contain underscore characters, periods, and non-consecutive forward slash characters"; public XElement XmlPackageData; public XElement XmlConfigData; private StringBuilder _progressMessage; public String ProgressMessage { get { return _progressMessage.ToString(); } set { _progressMessage.AppendLine(value); OnPropertyChanged("ProgressMessage"); } } private int _tabControlSelectedIndex; public int TabControlSelectedIndex { get { return _tabControlSelectedIndex; } set { _tabControlSelectedIndex = value; OnPropertyChanged("TabControlSelectedIndex"); } } private bool _areAllButtonsEnabled = true; public bool AreAllButtonsEnabled { get { return _areAllButtonsEnabled; } set { _areAllButtonsEnabled = value; OnPropertyChanged("AreAllButtonsEnabled"); } } public bool IsActiveConnectionSet { get { return (ActiveConfiguration != null) ? true : false; } } public bool IsActiveSolutionSet { get { return (ActiveSolution != null) ? true : false; } } public bool IsActivePackageSet { get { return (ActivePackage != null) ? true : false; } } private bool _shouldPublishAllAfterUpload; public bool ShouldPublishAllAfterUpload { get { return _shouldPublishAllAfterUpload; } set { _shouldPublishAllAfterUpload = value; OnPropertyChanged("ShouldPublishAllAfterUpload"); } } private bool _isOutputWindowDisplayed = false; public bool IsOutputWindowDisplayed { get { return _isOutputWindowDisplayed; } set { _isOutputWindowDisplayed = value; OnPropertyChanged("IsOutputWindowDisplayed"); OnPropertyChanged("IsWorkstationDisplayed"); } } public bool IsWorkstationDisplayed { get { return !(IsOutputWindowDisplayed); } } private String _fileSearchText; public String FileSearchText { get { return _fileSearchText; } set { _fileSearchText = value; OnPropertyChanged("FileSearchText"); } } //WebResources Packages public ObservableCollection<XElement> Packages { get; set; } private XElement _selectedPackage; public XElement SelectedPackage { get { return _selectedPackage; } set { _selectedPackage = value; OnPropertyChanged("SelectedPackage"); } } private XElement _activePackage; public XElement ActivePackage { get { return _activePackage; } set { _activePackage = value; OnPropertyChanged("ActivePackage"); OnPropertyChanged("IsActivePackageSet"); } } private bool _isActivePackageDirty = false; public bool IsActivePackageDirty { get { return _isActivePackageDirty; } set { _isActivePackageDirty = value; OnPropertyChanged("IsActivePackageDirty"); } } //FileInfos for all potential resources in a directory public ObservableCollection<FileInfo> CurrentFiles { get; set; } public ObservableCollection<FileInfo> CurrentFilesSelected { get; set; } //Represents a collection of "WebResourceInfo" node from XML. public ObservableCollection<XElement> WebResourceInfos { get; set; } public ObservableCollection<XElement> WebResourceInfosSelected { get; set; } //Connections public ObservableCollection<XElement> Configurations { get; set; } private XElement _selectedConfiguration; public XElement SelectedConfiguration { get { return _selectedConfiguration; } set { _selectedConfiguration = value; OnPropertyChanged("SelectedConfiguration"); } } private XElement _activeConfiguration; public XElement ActiveConfiguration { get { return _activeConfiguration; } set { _activeConfiguration = value; OnPropertyChanged("ActiveConfiguration"); OnPropertyChanged("IsActiveConnectionSet"); } } //Solutions public ObservableCollection<Solution> UnmanagedSolutions { get; set; } private Solution _selectedSolution; public Solution SelectedSolution { get { return _selectedSolution; } set { _selectedSolution = value; OnPropertyChanged("SelectedSolution"); } } private Solution _activeSolution; public Solution ActiveSolution { get { return _activeSolution; } set { _activeSolution = value; OnPropertyChanged("ActiveSolution"); OnPropertyChanged("IsActiveSolutionSet"); } } //Active Publisher private Publisher _activePublisher; public Publisher ActivePublisher { get { return _activePublisher; } set { _activePublisher = value; OnPropertyChanged("ActivePublisher"); } } private bool _shouldPublishAllUploadedWebResource; /// <summary> /// 是否更新已上传的WebResource /// </summary> public bool ShouldPublishAllUploadedWebResource { get { return _shouldPublishAllUploadedWebResource; } set { _shouldPublishAllUploadedWebResource = value; OnPropertyChanged("ShouldPublishAllUploadedWebResource"); } } #endregion #region Fields //CRM Data Provider private ConsolelessServerConnection _serverConnect; private static OrganizationServiceProxy _serviceProxy; private static OrganizationServiceContext _orgContext; BackgroundWorker worker; #endregion #region Constructor public MainWindowViewModel() { XDocument xmlPackagesDocument = XDocument.Load(PACKAGES_FILENAME); XmlPackageData = xmlPackagesDocument.Element("UtilityRoot"); XDocument xmlConfigurationsDocument = XDocument.Load(CONFIG_FILENAME); XmlConfigData = xmlConfigurationsDocument.Element("Configurations"); Configurations = new ObservableCollection<XElement>(); Packages = new ObservableCollection<XElement>(); UnmanagedSolutions = new ObservableCollection<Solution>(); CurrentFiles = new ObservableCollection<FileInfo>(); CurrentFilesSelected = new ObservableCollection<FileInfo>(); WebResourceInfos = new ObservableCollection<XElement>(); WebResourceInfosSelected = new ObservableCollection<XElement>(); //Begin loading the XML data LoadXmlData(); TabControlSelectedIndex = 0; _progressMessage = new StringBuilder(); _shouldPublishAllAfterUpload = false; _shouldPublishAllUploadedWebResource = true; List_UploadedWebResources = new List<Guid>(); //Set up the background worker to handle upload web resources. Helps //prevent the UI from locking up and can have a Console-like output window //with real-time display. worker = new BackgroundWorker(); worker.WorkerReportsProgress = true; worker.DoWork += new DoWorkEventHandler(BeginUpload); worker.RunWorkerCompleted += delegate(object s, RunWorkerCompletedEventArgs args) { this.BeginInvoke(() => { AreAllButtonsEnabled = true; }); }; } #endregion #region Methods private void LoadXmlData() { LoadConfigurations(); LoadPackages(); } //Configuration Methods private void LoadConfigurations() { Configurations.Clear(); var configs = XmlConfigData.Descendants("Configuration"); foreach (var c in configs) { Configurations.Add(c); } } private void SaveConfigurations() { XmlConfigData.Descendants("Configuration").Remove(); XmlConfigData.Add(Configurations.ToArray()); XmlConfigData.Save(CONFIG_FILENAME); } private void CreateNewConfiguration() { XElement newConfig = new XElement("Configuration", new XAttribute("name", "New Connection"), new XAttribute("server", String.Empty), new XAttribute("orgName", String.Empty), new XAttribute("userName", String.Empty), new XAttribute("domain", String.Empty)); Configurations.Add(newConfig); SelectedConfiguration = Configurations[Configurations.Count - 1]; } private void DeleteSelectedConfiguration() { if (SelectedConfiguration != null) { //if trying to delete the configuration that is also active already, //let them by clearing ActiveConfiguration and solutions. if (SelectedConfiguration == ActiveConfiguration) { ClearActiveConfiguration(); ClearSolutions(); } //Finally clear the SelectedConfiguration and remove it from the list of Configurations. var toBeDeleted = Configurations.Where(x => x == SelectedConfiguration).FirstOrDefault(); if (toBeDeleted != null) { Configurations.Remove(toBeDeleted); SelectedConfiguration = null; } } } private void ClearActiveConfiguration() { ActiveConfiguration = null; } private void ActivateSelectedConfiguration() { //User may have already been connected to another org, disconnect them. ClearActiveConfiguration(); //Clear out any Solutions from the Solutions collection since they are //Configuration specfic. ClearSolutions(); //Instantiate new proxy. if it is successful, it will also //set the ActiveConfiguration and retrieve Solutions. InstantiateService(); } private bool CanActivateSelectedConfiguration() { if (SelectedConfiguration != null && !String.IsNullOrWhiteSpace(SelectedConfiguration.Attribute("server").Value) && !String.IsNullOrWhiteSpace(SelectedConfiguration.Attribute("orgName").Value)) { return true; } return false; } //Solution Methods private void LoadSolutions() { //Check whether it already exists QueryExpression queryUnmanagedSolutions = new QueryExpression { EntityName = Solution.EntityLogicalName, ColumnSet = new ColumnSet(true), Criteria = new FilterExpression() }; queryUnmanagedSolutions.Criteria.AddCondition("ismanaged", ConditionOperator.Equal, false); EntityCollection querySolutionResults = _serviceProxy.RetrieveMultiple(queryUnmanagedSolutions); if (querySolutionResults.Entities.Count > 0) { //The Where() is important because a query for all solutions //where Type=Unmanaged returns 3 solutions. The CRM UI of a //vanilla instance shows only 1 unmanaged solution: "Default". //Assume "Active" and "Basic" should not be touched? UnmanagedSolutions = new ObservableCollection<Solution>( querySolutionResults.Entities .Select(x => x as Solution) .Where(s => s.UniqueName != "Active" && s.UniqueName != "Basic" ) ); //If only 1 solution returns just go ahead and default it. if (UnmanagedSolutions.Count == 1 && UnmanagedSolutions[0].UniqueName == "Default") { SelectedSolution = UnmanagedSolutions[0]; ActiveSolution = SelectedSolution; SetActivePublisher(); //Advance the user to the Packages TabItem TabControlSelectedIndex = 2; } else { //Advance the user to the Solutions TabItem TabControlSelectedIndex = 1; } OnPropertyChanged("UnmanagedSolutions"); OnPropertyChanged("SelectedSolution"); OnPropertyChanged("ActiveSolution"); } } private void InstantiateService() { try { if (SelectedConfiguration == null) throw new Exception("Please choose a configuration."); //Get the Password string password = String.Empty; PasswordWindow pw = new PasswordWindow(); pw.Owner = Application.Current.MainWindow; bool? submitted = pw.ShowDialog(); if (submitted.Value) { password = pw.GetPassword(); } else { ErrorWindow needPassword = new ErrorWindow("You need to supply a Password and Submit. Try again."); needPassword.Owner = Application.Current.MainWindow; needPassword.ShowDialog(); return; } _serverConnect = new ConsolelessServerConnection(); ConsolelessServerConnection.Configuration _config = new ConsolelessServerConnection.Configuration(); _config = _serverConnect.GetServerConfiguration( SelectedConfiguration.Attribute("server").Value, SelectedConfiguration.Attribute("orgName").Value, SelectedConfiguration.Attribute("userName").Value, password, SelectedConfiguration.Attribute("domain").Value); _serviceProxy = _serverConnect.GetOrganizationServiceProxy(_config.OrganizationUri,_config.EndpointType); // This statement is required to enable early-bound type support. _serviceProxy.ServiceConfiguration.CurrentServiceEndpoint.Behaviors .Add(new ProxyTypesBehavior()); // The OrganizationServiceContext is an object that wraps the service // proxy and allows creating/updating multiple records simultaneously. _orgContext = new OrganizationServiceContext(_serviceProxy); //Set the ActiveConnection ActiveConfiguration = SelectedConfiguration; //If all worked, retrieve the solutions. LoadSolutions(); } catch (Exception e) { StringBuilder sb = new StringBuilder(); sb.AppendLine(e.Message); sb.AppendLine(); sb.AppendLine("Please fix the Connection information and try again."); ErrorWindow errorWindow = new ErrorWindow(sb.ToString()); errorWindow.Owner = Application.Current.MainWindow; var x = errorWindow.ShowDialog(); } } private void ClearSolutions() { //Clear solutions UnmanagedSolutions.Clear(); SelectedSolution = null; ActiveSolution = null; ActivePublisher = null; } private void ActivateSelectedSolution() { if (SelectedSolution != null) { ActiveSolution = SelectedSolution; SetActivePublisher(); OnPropertyChanged("ActiveSolution"); //Advance the user to the Packages TabItem TabControlSelectedIndex = 2; } } private void SetActivePublisher() { if (ActiveSolution == null) return; var pub = from p in _orgContext.CreateQuery<Publisher>() where p.PublisherId.Value == ActiveSolution.PublisherId.Id select new Publisher { CustomizationPrefix = p.CustomizationPrefix }; ActivePublisher = pub.First(); OnPropertyChanged("ActivePublisher"); } //Package Methods private void LoadPackages() { Packages.Clear(); var packages = XmlPackageData.Element("Packages").Descendants("Package"); foreach (var p in packages) { Packages.Add(p); } OnPropertyChanged("Packages"); } private void SavePackages() { //The user is influenced to believe a Save event will only //save the ActivePackage but really it will save all of them. //Code is in place to prevent the user from editing one package then //trying to load another without saving the first. //At this point the XmlRootData object is stale and needs to be //repopulated with the Packages collection. XmlPackageData.Descendants("Package").Remove(); //But the ActivePackage may have its Web Resources modified and they //need to be added back to the ActivePackage. if (ActivePackage != null) { ActivePackage.Elements("WebResourceInfo").Remove(); ActivePackage.Add(WebResourceInfos.ToArray()); } XmlPackageData.Element("Packages").Add(Packages.ToArray()); XmlPackageData.Save(PACKAGES_FILENAME); IsActivePackageDirty = false; } private void DeleteSelectedPackage() { if (SelectedPackage != null) { var toBeDeleted = Packages.Where(x => x == SelectedPackage).FirstOrDefault(); if (toBeDeleted != null) { if (ActivePackage == SelectedPackage) { ActivePackage = null; //Also, clear out any dependencies CurrentFiles.Clear(); CurrentFilesSelected.Clear(); WebResourceInfos.Clear(); WebResourceInfosSelected.Clear(); } Packages.Remove(toBeDeleted); SelectedPackage = null; } SavePackages(); } } private void ActivatePackage() { //Don't allow them to load a package without first saving //the ActivePackage if its dirty. if (ActivePackage != null && IsActivePackageDirty) { ErrorWindow dirtyPackageWindow = new ErrorWindow("You have unsaved changes to the Active Package. Please save before loading another package."); dirtyPackageWindow.Owner = Application.Current.MainWindow; dirtyPackageWindow.ShowDialog(); return; } if (SelectedPackage != null) { ActivePackage = SelectedPackage; //Readies the Files DataGrid SearchAndPopulateFiles(); //Readies the Web Resources DataGrid LoadWebResourceInfos(); } } private void CreateNewPackage() { if (ActivePackage != null) { SavePackages(); } XElement newPackage = new XElement("Package", new XAttribute("name", "NewPackage"), new XAttribute("rootPath", String.Empty), new XAttribute("isNamePrefix", true)); Packages.Add(newPackage); SelectedPackage = Packages[Packages.Count - 1]; ActivatePackage(); SavePackages(); } private void LoadWebResourceInfos() { if (ActivePackage == null) return; //As always, clear the collection first. WebResourceInfos.Clear(); WebResourceInfosSelected.Clear(); var webResourceInfos = ActivePackage.Elements("WebResourceInfo"); if (webResourceInfos != null) { foreach (var wr in webResourceInfos) { WebResourceInfos.Add(wr); } } } private void SearchAndPopulateFiles() { if (ActivePackage == null) return; string searchText = FileSearchText; //Find all files string rootPath = ActivePackage.Attribute("rootPath").Value; DiscoverFiles(rootPath, searchText); } //Misc private void ShowBrowseFolderDialog() { System.Windows.Forms.FolderBrowserDialog dlgWRFolder = new System.Windows.Forms.FolderBrowserDialog() { Description = "Select the folder containing the potential Web Resource files", ShowNewFolderButton = false }; if (dlgWRFolder.ShowDialog() == System.Windows.Forms.DialogResult.OK) { IsActivePackageDirty = false; //Because Web Resources are relative to the root path, //all the current Web ResourceInfos should be cleared WebResourceInfos.Clear(); WebResourceInfosSelected.Clear(); //Change the rootpath and notify all bindings ActivePackage.Attribute("rootPath").Value = dlgWRFolder.SelectedPath; OnPropertyChanged("ActivePackage"); //Auto-save SavePackages(); //Display new files SearchAndPopulateFiles(); } } private void DiscoverFiles(string rootPath, string searchText) { CurrentFiles.Clear(); CurrentFilesSelected.Clear(); if (rootPath != String.Empty) { DirectoryInfo di = new DirectoryInfo(rootPath); var files = di.EnumerateFiles("*", SearchOption.AllDirectories) .Where(f => ResourceExtensions.ValidExtensions.Contains(f.Extension)) .Where(f => f.Name != PACKAGES_FILENAME); if (!string.IsNullOrWhiteSpace(searchText)) { files = files.Where(f => f.FullName.Contains(searchText)); } foreach (FileInfo f in files) { CurrentFiles.Add(f); } OnPropertyChanged("CurrentFiles"); } } private void AddFilesToWebResources(object parameter) { //Set the ActivePackage as Dirty. IsActivePackageDirty = true; //Clear the collection of selected files CurrentFilesSelected.Clear(); //List<FileInfo> selectedFiles = new List<FileInfo>(); if (parameter != null && parameter is IEnumerable) { foreach (var fileInfo in (IEnumerable)parameter) { CurrentFilesSelected.Add((FileInfo)fileInfo); } } if (CurrentFilesSelected.Count > 0) { foreach (FileInfo fi in CurrentFilesSelected) { //Add it to the list of web resource info, if not already there. //The matching criteria will be the ? XElement newInfo = ConvertFileInfoToWebResourceInfo(fi); if (WebResourceInfos.Where(w => w.Attribute("filePath").Value == newInfo.Attribute("filePath").Value).Count() == 0) { WebResourceInfos.Add(newInfo); } else { //it's already in the list! do nothing. } } } } private void DeleteSelectedWebResources(object parameter) { //Set the ActivePackage as Dirty. IsActivePackageDirty = true; WebResourceInfosSelected.Clear(); if (parameter != null && parameter is IEnumerable) { //Lists allow the ForEach extension method good for //removing items of a collection. Looping through an //enumerable caused indexing errors after the first //iteration. List<XElement> infosToDelete = new List<XElement>(); foreach (var wr in (IEnumerable)parameter) { infosToDelete.Add((XElement)wr); } infosToDelete.ForEach(info => WebResourceInfos.Remove(info)); } } private XElement ConvertFileInfoToWebResourceInfo(FileInfo fi) { var x = fi.Extension.Split('.'); string type = x[x.Length - 1].ToLower(); String name = fi.FullName.Replace(ActivePackage.Attribute("rootPath").Value.Replace("/", "\"), String.Empty); XElement newWebResourceInfo = new XElement("WebResourceInfo", new XAttribute("name", name.Replace("\", "/")), new XAttribute("filePath", name), new XAttribute("displayName", fi.Name), new XAttribute("type", type), new XAttribute("description", String.Empty)); return newWebResourceInfo; } private void BeginUpload(object s, DoWorkEventArgs args) { //Retrieve all Web Resources so we can determine if each //needs be created or updated. var crmResources = RetrieveWebResourcesForActiveSolution(); //Create or Update the WebResource if (WebResourceInfosSelected.Count > 0) { _progressMessage.Clear(); AddMessage(String.Format("Processing {0} Web Resources...", WebResourceInfosSelected.Count.ToString())); int i = 1; foreach (XElement fi in WebResourceInfosSelected) { string name = GetWebResourceFullNameIncludingPrefix(fi); AddMessage(String.Empty); AddMessage(i.ToString() + ") " + name); if (IsWebResourceNameValid(name)) { //If the Unmanaged Solution already contains the Web Resource, //do an Update. var resourceThatMayExist = crmResources.Where(w => w.Name == name).FirstOrDefault(); if (resourceThatMayExist != null) { AddMessage("Already exists. Updating..."); UpdateWebResource(fi, resourceThatMayExist); AddMessage("Done."); } //If not, create the Web Resource and a Solution Component. else { AddMessage("Doesn't exist. Creating..."); CreateWebResource(fi); AddMessage("Done."); } } else { AddMessage(VALID_NAME_MSG); } i++; } AddMessage(String.Empty); AddMessage("Done processing files."); //All WebResources should be in. Publish all. if (ShouldPublishAllAfterUpload) { AddMessage(String.Empty); AddMessage("You chose to publish all customizations. Please be patient as it may take a few minutes to complete."); PublishAll(); } else if (ShouldPublishAllUploadedWebResource) { if (List_UploadedWebResources.Count > 0) { AddMessage(String.Empty); AddMessage("You chose to publish uploaded webresources. Please be patient as it may take a few minutes to complete."); PublishUploadedWebResource(); } } else { AddMessage(String.Empty); AddMessage("You chose not to publish all customizations."); } AddMessage("Process complete!"); } } List<Guid> List_UploadedWebResources; private void PublishUploadedWebResource() { try { AddMessage(String.Empty); AddMessage("Publishing uploaded..."); PublishXmlRequest publishRequest = new PublishXmlRequest(); StringBuilder sb = new StringBuilder(); foreach (Guid webresId in List_UploadedWebResources) { sb.Append("<webresource>" + webresId.ToString() + "</webresource>"); } publishRequest.ParameterXml = string.Format(@"<importexportxml><webresources>{0}</webresources></importexportxml>" , sb.ToString()); var response = (PublishXmlResponse)_serviceProxy.Execute(publishRequest); AddMessage("Done."); } catch (Exception e) { AddMessage("Error publishing: " + e.Message); } } private void UploadWebResources(object parameter) { IsOutputWindowDisplayed = true; AreAllButtonsEnabled = false; //Clear the collection of selected Web Resources WebResourceInfosSelected.Clear(); //Clear the collection of uploaded Web Resources List_UploadedWebResources.Clear(); if (parameter != null && parameter is IEnumerable) { foreach (var webResource in (IEnumerable)parameter) { WebResourceInfosSelected.Add((XElement)webResource); } } worker.RunWorkerAsync(WebResourceInfosSelected); } private void UploadAllWebResources() { UploadWebResources(WebResourceInfos); } private bool CanUploadWebResource() { if (ActiveConfiguration != null && ActiveSolution != null && ActivePublisher != null && ActivePackage != null) { return true; } return false; } private void PublishAll() { try { AddMessage(String.Empty); AddMessage("Publishing all customizations..."); PublishAllXmlRequest publishRequest = new PublishAllXmlRequest(); var response = (PublishAllXmlResponse)_serviceProxy.Execute(publishRequest); AddMessage("Done."); } catch (Exception e) { AddMessage("Error publishing: " + e.Message); } } private IEnumerable<WebResource> RetrieveWebResourcesForActiveSolution() { //The following query finds all WebResources that are SolutionComponents for //the ActiveSolution. Simply querying WebResources does not retrieve the desired //results. Additionally, when creating WebResources, you must create a SolutionComponent //if attaching to any unmanaged solution other than the "Default Solution." var webResources = from wr in _orgContext.CreateQuery<WebResource>() join sc in _orgContext.CreateQuery<SolutionComponent>() on wr.WebResourceId equals sc.ObjectId where wr.IsManaged == false where wr.IsCustomizable.Value == true where sc.ComponentType.Value == (int)componenttype.WebResource where sc.SolutionId.Id == ActiveSolution.SolutionId.Value select new WebResource { WebResourceType = wr.WebResourceType, WebResourceId = wr.WebResourceId, DisplayName = wr.DisplayName, Name = wr.Name, // Content = wr.Content, Removed to improve performance Description = wr.Description }; return webResources.AsEnumerable(); } private void CreateWebResource(XElement webResourceInfo) { try { //Create the Web Resource. WebResource wr = new WebResource() { Content = getEncodedFileContents(ActivePackage.Attribute("rootPath").Value + webResourceInfo.Attribute("filePath").Value), DisplayName = webResourceInfo.Attribute("displayName").Value, Description = webResourceInfo.Attribute("description").Value, LogicalName = WebResource.EntityLogicalName, Name = GetWebResourceFullNameIncludingPrefix(webResourceInfo) }; wr.WebResourceType = new OptionSetValue((int)ResourceExtensions.ConvertStringExtension(webResourceInfo.Attribute("type").Value)); //Special cases attributes for different web resource types. switch (wr.WebResourceType.Value) { case (int)ResourceExtensions.WebResourceType.Silverlight: wr.SilverlightVersion = "4.0"; break; } // ActivePublisher.CustomizationPrefix + "_/" + ActivePackage.Attribute("name").Value + webResourceInfo.Attribute("name").Value.Replace("\", "/"), Guid theGuid = _serviceProxy.Create(wr); List_UploadedWebResources.Add(theGuid); //If not the "Default Solution", create a SolutionComponent to assure it gets //associated with the ActiveSolution. Web Resources are automatically added //as SolutionComponents to the Default Solution. if (ActiveSolution.UniqueName != "Default") { AddSolutionComponentRequest scRequest = new AddSolutionComponentRequest(); scRequest.ComponentType = (int)componenttype.WebResource; scRequest.SolutionUniqueName = ActiveSolution.UniqueName; scRequest.ComponentId = theGuid; var response = (AddSolutionComponentResponse)_serviceProxy.Execute(scRequest); } } catch (Exception e) { AddMessage("Error: " + e.Message); return; } } private void UpdateWebResource(XElement webResourceInfo, WebResource existingResource) { try { //These are the only 3 things that should (can) change. WebResource wr = new WebResource() { Content = getEncodedFileContents(ActivePackage.Attribute("rootPath").Value + webResourceInfo.Attribute("filePath").Value), DisplayName = webResourceInfo.Attribute("displayName").Value, Description = webResourceInfo.Attribute("description").Value }; wr.WebResourceId = existingResource.WebResourceId; _serviceProxy.Update(wr); List_UploadedWebResources.Add(existingResource.WebResourceId.Value); } catch (Exception e) { AddMessage("Error: " + e.Message); return; } } private string getEncodedFileContents(String pathToFile) { FileStream fs = new FileStream(pathToFile, FileMode.Open, FileAccess.Read); byte[] binaryData = new byte[fs.Length]; long bytesRead = fs.Read(binaryData, 0, (int)fs.Length); fs.Close(); #region Custom Modification return System.Convert.ToBase64String(binaryData, 0, binaryData.Length); //byte[] convertedBytes = Encoding.Convert(Encoding.UTF8, Encoding.GetEncoding("GB2312"), binaryData); //return System.Convert.ToBase64String(convertedBytes, 0, convertedBytes.Length); #endregion } private string GetWebResourceFullNameIncludingPrefix(XElement webResourceInfo) { //The Web Resource name always starts with the Publisher's Prefix //i.e., "new_" string name = ActivePublisher.CustomizationPrefix + "_"; //Check to see if the user has chosen to add the Package Name as part of the //prefix. if (!String.IsNullOrWhiteSpace(ActivePackage.Attribute("isNamePrefix").Value) && Boolean.Parse(ActivePackage.Attribute("isNamePrefix").Value) == true) { name += "/" + ActivePackage.Attribute("name").Value; } //Finally add the name on to the prefix name += webResourceInfo.Attribute("name").Value; return name; } private bool IsWebResourceNameValid(string name) { Regex inValidWRNameRegex = new Regex("[^a-z0-9A-Z_\./]|[/]{2,}", (RegexOptions.Compiled | RegexOptions.CultureInvariant)); bool result = true; //Test valid characters if (inValidWRNameRegex.IsMatch(name)) { AddMessage(VALID_NAME_MSG); result = false; } //Test length //Remove the customization prefix and leading _ if (name.Remove(0, ActivePublisher.CustomizationPrefix.Length + 1).Length > 100) { AddMessage("ERROR: Web Resource name must be <= 100 characters."); result = false; } return result; } private void AddMessage(string msg) { //Assures that this happens on the UI thread this.BeginInvoke(() => { ProgressMessage = msg; }); } #endregion } }
WebResourceView.xaml
1 <UserControl 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 5 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 6 mc:Ignorable="d" 7 x:Class="Microsoft.Crm.Sdk.Samples.WebResourceView" 8 xmlns:view="clr-namespace:Microsoft.Crm.Sdk.Samples" 9 x:Name="UserControl" d:DesignWidth="500" 10 > 11 12 <UserControl.Resources> 13 <view:BoolToVisibilityConverter x:Key="boolToVisibilityConverter" /> 14 </UserControl.Resources> 15 <Border x:Name="LayoutRoot" CornerRadius="8,8,8,8" Background="LightGray" > 16 <Grid> 17 <Grid.RowDefinitions> 18 <RowDefinition Height="Auto" /> 19 <RowDefinition Height="Auto" /> 20 <RowDefinition Height="Auto" /> 21 <RowDefinition Height="Auto" /> 22 <RowDefinition Height="Auto" /> 23 <RowDefinition Height="Auto" /> 24 </Grid.RowDefinitions> 25 26 <TextBlock Text="Load an existing package or create a new one." 27 Margin="4" 28 FontWeight="Bold" 29 FontSize="18.667" 30 FontFamily="/WebResourceUtility;component/Fonts/#Arial" > 31 </TextBlock> 32 33 <TextBlock Grid.Row="1" 34 Text="Packages" 35 FontWeight="Bold" 36 Margin="4,10,4,0" /> 37 38 <!-- Packages --> 39 <ScrollViewer Grid.Row="2" VerticalScrollBarVisibility="Auto" Height="Auto" MaxHeight="120" Margin="4,0,4,0" > 40 <DataGrid x:Name="WebResourcePackagesGrid" 41 IsManipulationEnabled="False" 42 ItemsSource="{Binding Path=Packages}" 43 SelectedItem="{Binding Path=SelectedPackage, Mode=TwoWay}" 44 SelectionMode="Single" 45 AutoGenerateColumns="False" 46 IsReadOnly="True" > 47 <DataGrid.Columns> 48 <DataGridTextColumn Header="Package Name" Binding="{Binding Path=Attribute[name].Value}" Width="Auto" MinWidth="120" /> 49 <DataGridTextColumn Header="Root Path" Binding="{Binding Path=Attribute[rootPath].Value}" Width="*" /> 50 </DataGrid.Columns> 51 </DataGrid> 52 </ScrollViewer> 53 54 <!-- Package Commands --> 55 <StackPanel Orientation="Horizontal" Grid.Row="3" Margin="0,0,0,20" 56 57 HorizontalAlignment="Stretch"> 58 <Button x:Name="LoadPackageButton" 59 Command="{Binding Path=ActivateSelectedPackageCommand}" 60 Content="Load Package" 61 Margin="4,2,0,0" /> 62 63 <Button x:Name="NewPackageButton" Command="{Binding Path=CreateNewPackageCommand}" Content="New Package" Margin="4,2,0,0" Height="20.277" VerticalAlignment="Top"/> 64 <Button x:Name="DeletePackageButton" Command="{Binding Path=DeleteActivePackageCommand}" Content="Delete" Margin="4,2,4,0" /> 65 </StackPanel> 66 67 68 <!-- Package Workstation --> 69 <Border x:Name="OutputBorder" 70 Grid.Row="4" 71 Margin="4,0,4,8" 72 CornerRadius="8,8,8,8" 73 Background="DarkGray" 74 Visibility="{Binding Path=IsActivePackageSet, Converter={StaticResource boolToVisibilityConverter}}"> 75 <Grid> 76 <Grid Grid.Row="0" Visibility="{Binding Path=IsWorkstationDisplayed, Converter={StaticResource boolToVisibilityConverter}}"> 77 <Grid.RowDefinitions> 78 <RowDefinition Height="Auto"/> 79 <RowDefinition Height="Auto"/> 80 <RowDefinition Height="Auto"/> 81 <RowDefinition Height="Auto"/> 82 <RowDefinition Height="Auto"/> 83 <RowDefinition Height="Auto"/> 84 </Grid.RowDefinitions> 85 <Grid.ColumnDefinitions> 86 <ColumnDefinition /> 87 <ColumnDefinition /> 88 <ColumnDefinition /> 89 <ColumnDefinition /> 90 </Grid.ColumnDefinitions> 91 92 <Label Margin="4" HorizontalAlignment="Right" FontWeight="Bold">Package Name</Label> 93 <TextBox Margin="4" 94 Grid.Column="1" 95 Grid.ColumnSpan="2" 96 ToolTip="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" 97 > 98 <TextBox.Text> 99 <Binding Path="ActivePackage.Attribute[name].Value" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" > 100 <Binding.ValidationRules> 101 <view:RegexValidationRule Pattern="[^a-z0-9A-Z_\./]|[/]{2,}" Message="Error: Can only contain alphnumeric, periods, underscores, and single forward slash" /> 102 </Binding.ValidationRules> 103 </Binding> 104 </TextBox.Text> 105 </TextBox> 106 <Label Grid.Row="1" HorizontalAlignment="Right" Margin="4" FontWeight="Bold">Root Path</Label> 107 <TextBox IsEnabled="False" Text="{Binding Path=ActivePackage.Attribute[rootPath].Value, Mode=TwoWay}" 108 Grid.Row="1" 109 Grid.Column="1" 110 Grid.ColumnSpan="2" 111 Margin="4"/> 112 <Button x:Name="BrowseButton" 113 Command="{Binding BrowseFolderCommand}" 114 Content="Browse" 115 Grid.Row="1" 116 Margin="2,4,0,4" 117 Grid.Column="3" 118 Width="60" 119 HorizontalAlignment="Left"/> 120 121 122 123 <!-- File Search Section --> 124 <GroupBox Grid.Row="2" 125 Grid.ColumnSpan="4" 126 Margin="4" > 127 <GroupBox.Header> 128 <TextBlock Text="File Search" FontWeight="Bold" /> 129 </GroupBox.Header> 130 <Grid> 131 <Grid.RowDefinitions> 132 <RowDefinition Height="Auto"/> 133 <RowDefinition Height="Auto"/> 134 <RowDefinition Height="Auto"/> 135 <RowDefinition Height="*"/> 136 <RowDefinition Height="Auto"/> 137 </Grid.RowDefinitions> 138 <Grid.ColumnDefinitions> 139 <ColumnDefinition Width="Auto" /> 140 <ColumnDefinition Width="*" /> 141 <ColumnDefinition Width="Auto" /> 142 </Grid.ColumnDefinitions> 143 144 <Label VerticalAlignment="Bottom" HorizontalAlignment="Right" FontWeight="Bold">Search by Filename:</Label> 145 146 <TextBox x:Name="FileNameTextBox" 147 Text="{Binding Path=FileSearchText, Mode=TwoWay}" 148 Grid.Row="0" 149 Grid.Column="1" 150 /> 151 152 <Button x:Name="RefreshFilesButton" 153 Margin="4,0,0,0" 154 Width="100" 155 Command="{Binding Path=RefreshFilesCommand}" 156 Content="Search" 157 Grid.Row="0" 158 Grid.Column="3" 159 /> 160 161 <DataGrid x:Name="FileDataGrid" 162 IsReadOnly="True" 163 SelectionMode="Extended" 164 SelectionUnit="FullRow" 165 ItemsSource="{Binding Path=CurrentFiles}" 166 AutoGenerateColumns="False" 167 Grid.Row="3" 168 Grid.ColumnSpan="3" 169 Height="140" 170 Margin="0,4,0,0" > 171 <DataGrid.Columns> 172 <DataGridTextColumn Header="Type" Binding="{Binding Path=Extension}" /> 173 <DataGridTextColumn Header="File Name" Binding="{Binding Path=Name}" /> 174 <DataGridTextColumn Header="Path" Binding="{Binding Path=FullName}"/> 175 </DataGrid.Columns> 176 </DataGrid> 177 178 179 180 <!-- The DataGrid's SelectedItems property is read-only and can not be 181 bound via XAML. We need to have the SelectedItems to know which 182 to add to the Web Resource datagrid. 183 Workaround: 184 When the user clicks the Add Web Resource button, send a CommandParameter 185 along with the Command that is bound to the grid's SelectedItems. In 186 the RelayCommand, extract the Items and update the ViewModel accordingly. 187 NOTE: This will only keep the ViewModel synced with the selectedrows when 188 the button is clicked. Any changes thereafter are unrecognized until clicked 189 again 190 --> 191 192 <Button x:Name="AddFileButton" 193 Command="{Binding Path=ConvertFileToResourceCommand}" 194 CommandParameter="{Binding ElementName=FileDataGrid, Path=SelectedItems}" 195 Margin="0,2,0,4" 196 Content="Add Files to Web Resources" 197 Grid.Row="4" 198 Grid.Column="0"/> 199 </Grid> 200 </GroupBox> 201 202 <!-- Web Resources Section --> 203 <GroupBox 204 Grid.Row="3" 205 Grid.ColumnSpan="4" 206 Margin="4,4,4,8"> 207 <GroupBox.Header> 208 <TextBlock Text="Web Resources" FontWeight="Bold" /> 209 </GroupBox.Header> 210 <Grid> 211 <Grid.RowDefinitions> 212 <RowDefinition Height="Auto" /> 213 <RowDefinition Height="Auto" /> 214 <RowDefinition Height="*"/> 215 <RowDefinition Height="Auto"/> 216 217 </Grid.RowDefinitions> 218 <Grid.ColumnDefinitions> 219 <ColumnDefinition Width="Auto" /> 220 <ColumnDefinition Width="*" /> 221 </Grid.ColumnDefinitions> 222 223 <CheckBox Grid.Row="1" x:Name="PackageNamePrefixCheckBox" Content="Use Package Name as Web Resource namespace" 224 IsChecked="{Binding Path=ActivePackage.Attribute[isNamePrefix].Value}" 225 Margin="0,4,0,0" 226 Grid.ColumnSpan="2"/> 227 228 <StackPanel Grid.ColumnSpan="2" Orientation="Horizontal"> 229 <TextBlock >Web Resource name prefix:</TextBlock> 230 <TextBlock Margin="4,0,0,0" 231 Text="{Binding Path=ActivePublisher.CustomizationPrefix}"></TextBlock> 232 <TextBlock Text="_" 233 Visibility="{Binding Path=IsActiveSolutionSet, Converter={StaticResource boolToVisibilityConverter}}"/> 234 <TextBlock Text="/" 235 Visibility="{Binding ElementName=PackageNamePrefixCheckBox, Path=IsChecked, Converter={StaticResource boolToVisibilityConverter}}" /> 236 <TextBlock Text="{Binding Path=ActivePackage.Attribute[name].Value}" 237 Visibility="{Binding ElementName=PackageNamePrefixCheckBox, Path=IsChecked, Converter={StaticResource boolToVisibilityConverter}}"/> 238 239 </StackPanel> 240 241 242 <DataGrid x:Name="WebResourcesDataGrid" 243 ItemsSource="{Binding Path=WebResourceInfos}" 244 SelectionMode="Extended" 245 AutoGenerateColumns="False" 246 Grid.Row="2" 247 Grid.ColumnSpan="2" 248 Height="140" Margin="0,4,0,0"> 249 <DataGrid.Columns> 250 <DataGridTextColumn IsReadOnly="True" Header="Type" Binding="{Binding Path=Attribute[type].Value}" /> 251 <DataGridTextColumn Header="Name (editable)" Binding="{Binding Path=Attribute[name].Value}" /> 252 <DataGridTextColumn Header="Display Name (editable)" Binding="{Binding Path=Attribute[displayName].Value, Mode=TwoWay}" /> 253 <DataGridTextColumn Header="Description (editable)" Binding="{Binding Path=Attribute[description].Value, Mode=TwoWay}" /> 254 </DataGrid.Columns> 255 </DataGrid> 256 257 <Button Grid.Row="3" x:Name="RemoveWebResourceButton" Margin="0,2,0,0" 258 Content="Remove Web Resource" 259 Command="{Binding Path=DeleteWebResourcesCommand}" 260 CommandParameter="{Binding ElementName=WebResourcesDataGrid, Path=SelectedItems}" 261 /> 262 263 </Grid> 264 </GroupBox> 265 266 <!-- Publish options--> 267 <CheckBox HorizontalAlignment="Left" Margin="4" 268 Grid.Row="4" Grid.ColumnSpan="4" 269 IsChecked="{Binding Path=ShouldPublishAllUploadedWebResource, Mode=TwoWay}" 270 Content="Publish uploaded web resources." /> 271 272 <CheckBox HorizontalAlignment="Right" Margin="4" 273 Grid.Row="4" Grid.ColumnSpan="4" 274 IsChecked="{Binding Path=ShouldPublishAllAfterUpload, Mode=TwoWay}" 275 Content="Publish all customizations after uploading web resources." /> 276 277 <!-- Package Commands --> 278 <StackPanel 279 Grid.Row="5" 280 Grid.Column="0" 281 Grid.ColumnSpan="4" 282 Orientation="Horizontal" 283 HorizontalAlignment="Center" > 284 <Button x:Name="SavePackageButton" 285 Command="{Binding Path=SaveActivePackageCommand}" 286 Content="Save Package" 287 Margin="4" 288 Grid.Row="4" 289 Grid.ColumnSpan="4" Width="120" Height="30"/> 290 <Button x:Name="DeployAllButton" 291 Width="120" 292 Command="{Binding Path=UploadAllWebResourcesCommand}" 293 Margin="4" 294 Content="Upload All" 295 /> 296 <Button x:Name="DeployWebResourcesButton" 297 Command="{Binding Path=UploadWebResourcesCommand}" 298 Width="120" 299 CommandParameter="{Binding ElementName=WebResourcesDataGrid, Path=SelectedItems}" 300 Margin="4" 301 Content="Upload Selected" 302 /> 303 <Button Command="{Binding Path=ShowOutputWindow}" 304 HorizontalAlignment="Right" 305 Margin="4" 306 Content="Show Output" 307 /> 308 </StackPanel> 309 310 </Grid> 311 <Grid Grid.Row="0" 312 Visibility="{Binding Path=IsOutputWindowDisplayed, Converter={StaticResource boolToVisibilityConverter}}" 313 MaxHeight="{Binding ElementName=OutputBorder, Path=ActualHeight}"> 314 <Grid.RowDefinitions> 315 <RowDefinition Height="Auto" /> 316 <RowDefinition Height="*" /> 317 <RowDefinition Height="Auto" /> 318 </Grid.RowDefinitions> 319 <TextBlock Text="Output" FontWeight="Bold" Margin="4" /> 320 <ScrollViewer Grid.Row="1" 321 Margin="4" 322 HorizontalScrollBarVisibility="Visible" 323 VerticalScrollBarVisibility="Visible" > 324 <TextBox IsReadOnly="True" Text="{Binding Path=ProgressMessage}" /> 325 </ScrollViewer> 326 <Button Grid.Row="2" Content="Hide Output" Command="{Binding Path=HideOutputWindow}" Width="140" Margin="4" /> 327 </Grid> 328 </Grid> 329 </Border> 330 331 332 </Grid> 333 </Border> 334 335 </UserControl>