From 507b0c2114700181a6cf5c924c57238480e74c80 Mon Sep 17 00:00:00 2001 From: dd <535915157@qq.com> Date: Tue, 14 Dec 2021 14:10:44 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=89=8D=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E6=A1=86=E6=9E=B6=E6=BA=90=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vs/IoTGateway/DesignTimeBuild/.dtbcache.v2 | Bin 297944 -> 394265 bytes .vs/IoTGateway/project-colors.json | 22 +- .vs/IoTGateway/v17/.futdcache.v1 | Bin 1739 -> 2261 bytes .vs/IoTGateway/v17/.suo | Bin 148992 -> 156672 bytes .vs/IoTGateway/v17/fileList.bin | Bin 70012 -> 102833 bytes IoTGateway.Model/IoTGateway.Model.csproj | 6 +- IoTGateway.sln | 23 + IoTGateway/IoTGateway.csproj | 4 +- .../Attributes/ActionDescriptionAttribute.cs | 46 + .../Attributes/AllRightsAttribute.cs | 12 + .../Attributes/DebugOnlyAttribute.cs | 11 + .../Attributes/FixConnectionAttribute.cs | 50 + .../Attributes/MiddleTableAttribute.cs | 15 + .../Attributes/NoLogAttribute.cs | 11 + .../Attributes/PublicAttribute.cs | 14 + .../Attributes/ReInitAttribute.cs | 27 + .../ValidateFormItemOnlyAttribute.cs | 11 + .../WalkingTec.Mvvm.Core/BaseBatchVM.cs | 419 +++ .../WalkingTec.Mvvm.Core/BaseCRUDVM.cs | 1054 +++++++ .../WalkingTec.Mvvm.Core/BaseImportVM.cs | 1443 +++++++++ .../WalkingTec.Mvvm.Core/BasePagedListVM.cs | 1228 ++++++++ .../WalkingTec.Mvvm.Core/BaseSearcher.cs | 196 ++ .../WalkingTec.Mvvm.Core/BaseTemplateVM.cs | 345 +++ .../WalkingTec.Mvvm.Core/BaseVM.cs | 284 ++ .../WalkingTec.Mvvm.Core/ConfigOptions/CS.cs | 59 + .../ConfigOptions/Configs.cs | 480 +++ .../ConfigOptions/CookieOptions.cs | 17 + .../ConfigOptions/Cors.cs | 18 + .../WalkingTec.Mvvm.Core/ConfigOptions/DFS.cs | 35 + .../ConfigOptions/DFSTracker.cs | 18 + .../ConfigOptions/DefaultConfigConsts.cs | 38 + .../ConfigOptions/Domain.cs | 46 + .../ConfigOptions/FileUploadOptions.cs | 34 + .../ConfigOptions/JwtOptions.cs | 14 + .../WalkingTec.Mvvm.Core/ConfigOptions/KV.cs | 19 + .../ConfigOptions/UEditorOptions.cs | 386 +++ .../ConfigOptions/UIOptions.cs | 49 + .../WalkingTec.Mvvm.Core/CoreProgram.cs | 34 + .../WalkingTec.Mvvm.Core/DataContext.cs | 1269 ++++++++ WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Enums.cs | 156 + .../Exceptions/NullOrEmptyStringException.cs | 17 + .../Extensions/ConfigExtension.cs | 192 ++ .../Extensions/DCExtension.cs | 1112 +++++++ .../Extensions/DistinctExtension.cs | 15 + .../Extensions/ITreeDataExtension.cs | 161 + .../Extensions/ListExtension.cs | 178 ++ .../Extensions/ListVMExtension.cs | 377 +++ .../Extensions/LoginUserInfoExtension.cs | 37 + .../Extensions/PagedListExtension.cs | 323 ++ .../SystemExtensions/DateTimeHelper.cs | 81 + .../DistributedCacheExtensions.cs | 158 + .../SystemExtensions/EnumExtension.cs | 91 + .../SystemExtensions/StringExtension.cs | 230 ++ .../SystemExtensions/SystemExtension.cs | 67 + .../SystemExtensions/TypeExtension.cs | 527 ++++ .../WalkingTec.Mvvm.Core/GlobalConstants.cs | 10 + .../WalkingTec.Mvvm.Core/GlobalData.cs | 59 + .../WalkingTec.Mvvm.Core/GlobalServices.cs | 70 + .../WalkingTec.Mvvm.Core/Grid/GridAction.cs | 224 ++ .../Grid/GridActionExtension.cs | 424 +++ .../Grid/GridActionExtension`Old.cs | 205 ++ .../WalkingTec.Mvvm.Core/Grid/GridColumn.cs | 510 +++ .../Grid/GridColumnExtension`Old.cs | 218 ++ .../Grid/GridHeaderExtension.cs | 310 ++ .../WalkingTec.Mvvm.Core/Grid/IGridColumn.cs | 225 ++ .../Helper/EntityHelper.cs | 166 + .../Helper/IServiceExtension.cs | 66 + .../WalkingTec.Mvvm.Core/Helper/LogDebug.cs | 62 + .../WalkingTec.Mvvm.Core/Helper/LogTrace.cs | 126 + .../Helper/PropertyHelper.cs | 826 +++++ .../WalkingTec.Mvvm.Core/IBasePagedListVM.cs | 162 + .../WalkingTec.Mvvm.Core/IBaseVM.cs | 85 + .../WalkingTec.Mvvm.Core/IDataContext.cs | 143 + .../WalkingTec.Mvvm.Core/ISessionService.cs | 12 + .../WalkingTec.Mvvm.Core/IUIService.cs | 28 + .../Implement/DefaultUIService.cs | 59 + .../Json/BodyConverter.cs | 105 + .../Json/BoolStringConverter.cs | 62 + .../Json/DateRangeConverter.cs | 61 + .../Json/DateTimeConverter.cs | 29 + .../Json/DynamicDataConverter.cs | 90 + .../Json/PocoConverter.cs | 163 + .../Json/RawStringConverter.cs | 49 + .../Json/StringConverter.cs | 40 + .../Json/StringIgnoreLTGTConverter.cs | 40 + .../Json/TypeConverter.cs | 25 + .../WalkingTec.Mvvm.Core/JsonResultModel.cs | 38 + .../WalkingTec.Mvvm.Core/LoginUserInfo.cs | 143 + WalkingTec.Mvvm/WalkingTec.Mvvm.Core/MSD.cs | 104 + .../WalkingTec.Mvvm.Core/Models/ActionLog.cs | 77 + .../WalkingTec.Mvvm.Core/Models/BasePoco.cs | 44 + .../Models/DataPrivilege.cs | 30 + .../Models/FileAttachment.cs | 60 + .../Models/FrameworkGroup.cs | 35 + .../Models/FrameworkMenu.cs | 80 + .../Models/FrameworkRole.cs | 39 + .../Models/FrameworkUser.cs | 48 + .../Models/FrameworkUserGroup.cs | 19 + .../Models/FrameworkUserRole.cs | 18 + .../Models/FunctionPrivilege.cs | 26 + .../WalkingTec.Mvvm.Core/Models/ISearcher.cs | 90 + .../WalkingTec.Mvvm.Core/Models/ISubFile.cs | 13 + .../WalkingTec.Mvvm.Core/Models/IWtmFile.cs | 26 + .../Models/PersistPoco.cs | 21 + .../Models/PersistedGrant.cs | 34 + .../Models/TopBasePoco.cs | 116 + .../Models/TrackingObj.cs | 15 + .../WalkingTec.Mvvm.Core/Models/TreePoco.cs | 39 + .../Support/AuthConstants.cs | 314 ++ .../WalkingTec.Mvvm.Core/Support/ChartData.cs | 22 + .../Support/ClaimComparer.cs | 102 + .../Support/ColumnFormatInfo.cs | 135 + .../Support/CommonEqualityComparer.cs | 26 + .../Support/DataPrivilegeInfo.cs | 123 + .../WalkingTec.Mvvm.Core/Support/DateRange.cs | 451 +++ .../Support/DuplicateInfo.cs | 341 ++ .../Support/DynamicData.cs | 63 + .../Support/ExcelPropety.cs | 497 +++ .../Support/ExpressionVisitors.cs | 676 ++++ .../Support/ExtraClass.cs | 68 + .../Support/FileHandlers/IWtmFileHandler.cs | 17 + .../FileHandlers/WtmDataBaseFileHandler.cs | 57 + .../FileHandlers/WtmFileHandlerBase.cs | 43 + .../Support/FileHandlers/WtmFileProvider.cs | 205 ++ .../FileHandlers/WtmLocalFileHandler.cs | 105 + .../Support/FileHandlers/WtmOssFileHandler.cs | 140 + .../Support/Json/SimpleAction.cs | 67 + .../Support/Json/SimpleArea.cs | 16 + .../Support/Json/SimpleDataPri.cs | 19 + .../Support/Json/SimpleFunctionPri.cs | 18 + .../Support/Json/SimpleGroup.cs | 13 + .../Support/Json/SimpleLog.cs | 63 + .../Support/Json/SimpleMenu.cs | 41 + .../Support/Json/SimpleModule.cs | 55 + .../Support/Json/SimpleRole.cs | 13 + .../Support/Json/SimpleUserInfo.cs | 26 + .../WalkingTec.Mvvm.Core/Support/ListItem.cs | 69 + .../WalkingTec.Mvvm.Core/Support/NugetInfo.cs | 22 + .../Support/SupportedGroupMapping.cs | 32 + .../Support/TypeComparer.cs | 15 + .../WalkingTec.Mvvm.Core/Support/WTMLogger.cs | 149 + .../WalkingTec.Mvvm.Core/Support/WebProxy.cs | 33 + .../Support/WtmLocalizationOption.cs | 12 + WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Utils.cs | 824 +++++ .../WalkingTec.Mvvm.Core/WTMContext.cs | 966 ++++++ .../WalkingTec.Mvvm.Core.csproj | 45 + .../Attribute/AuthorizeCookieAttribute.cs | 14 + .../Auth/Attribute/AuthorizeJwtAttribute.cs | 14 + .../AuthorizeJwtWithCookieAttribute.cs | 14 + .../Auth/JwtAuth/ITokenService.cs | 30 + .../WalkingTec.Mvvm.Mvc/Auth/JwtAuth/Token.cs | 20 + .../Auth/JwtAuth/TokenService.cs | 155 + .../WalkingTec.Mvvm.Mvc/BaseApiController.cs | 313 ++ .../WalkingTec.Mvvm.Mvc/BaseController.cs | 435 +++ .../Binders/DateRangeBinder.cs | 56 + .../WalkingTec.Mvvm.Mvc/Binders/EnumBinder.cs | 55 + .../Binders/StringBinderProvider.cs | 36 + .../Binders/StringIgnoreLTGTBinder.cs | 61 + .../Binders/StringNeedLTGTAttribute.cs | 12 + .../WalkingTec.Mvvm.Mvc/CodeGenListVM.cs | 272 ++ .../WalkingTec.Mvvm.Mvc/CodeGenVM.cs | 2737 +++++++++++++++++ .../Filters/DataContextFilter.cs | 78 + .../Filters/FrameworkFilter.cs | 433 +++ .../Filters/PrivilegeFilter.cs | 247 ++ .../Filters/SwaggerFilter.cs | 35 + .../GeneratorFiles/ApiTest.txt | 130 + .../GeneratorFiles/ApiTestTopPoco.txt | 126 + .../GeneratorFiles/BatchVM.txt | 35 + .../GeneratorFiles/ControllerTest.txt | 213 ++ .../GeneratorFiles/ControllerTestTopPoco.txt | 196 ++ .../GeneratorFiles/CrudVM.txt | 39 + .../GeneratorFiles/HeaderFormat.txt | 9 + .../GeneratorFiles/ImportVM.txt | 27 + .../GeneratorFiles/ListVM.txt | 40 + .../GeneratorFiles/Mvc/BatchDeleteView.txt | 12 + .../GeneratorFiles/Mvc/BatchEditView.txt | 12 + .../GeneratorFiles/Mvc/Controller.txt | 219 ++ .../GeneratorFiles/Mvc/CreateView.txt | 9 + .../GeneratorFiles/Mvc/DeleteView.txt | 11 + .../GeneratorFiles/Mvc/DetailsView.txt | 8 + .../GeneratorFiles/Mvc/EditView.txt | 10 + .../GeneratorFiles/Mvc/ImportView.txt | 14 + .../GeneratorFiles/Mvc/ListView.txt | 6 + .../GeneratorFiles/Searcher.txt | 21 + .../GeneratorFiles/Spa/Blazor/Controller.txt | 168 + .../GeneratorFiles/Spa/Blazor/Create.txt | 38 + .../GeneratorFiles/Spa/Blazor/Details.txt | 34 + .../GeneratorFiles/Spa/Blazor/Edit.txt | 41 + .../GeneratorFiles/Spa/Blazor/Import.txt | 66 + .../GeneratorFiles/Spa/Blazor/Index.txt | 144 + .../GeneratorFiles/Spa/Controller.txt | 168 + .../GeneratorFiles/Spa/React/index.txt | 21 + .../GeneratorFiles/Spa/React/store/index.txt | 51 + .../GeneratorFiles/Spa/React/style.txt | 2 + .../GeneratorFiles/Spa/React/views/action.txt | 162 + .../GeneratorFiles/Spa/React/views/forms.txt | 77 + .../GeneratorFiles/Spa/React/views/models.txt | 51 + .../GeneratorFiles/Spa/React/views/other.txt | 17 + .../GeneratorFiles/Spa/React/views/search.txt | 32 + .../GeneratorFiles/Spa/React/views/table.txt | 20 + .../GeneratorFiles/Spa/Vue/config.txt | 16 + .../GeneratorFiles/Spa/Vue/index.txt | 55 + .../GeneratorFiles/Spa/Vue/store/api.txt | 70 + .../GeneratorFiles/Spa/Vue/store/index.txt | 9 + .../Spa/Vue/views/dialog-form.txt | 40 + .../Helper/ActionDescriptionExtension.cs | 29 + .../Helper/ActionExecutingContextExtension.cs | 32 + .../WalkingTec.Mvvm.Mvc/Helper/FResult.cs | 25 + .../Helper/FResultExtension.cs | 87 + .../Helper/FileExtension.cs | 32 + .../Helper/FrameworkServiceExtension.cs | 889 ++++++ .../Helper/HttpContextExtention.cs | 27 + .../Helper/IconFontsHelper.cs | 108 + .../Helper/ModelStateExtension.cs | 36 + .../Helper/ModelStateServiceProvider.cs | 73 + .../Helper/MvcOptionExtension.cs | 85 + .../Helper/SessionExtension.cs | 21 + .../Helper/SessionServiceProvider.cs | 34 + .../Helper/WtmContextOption.cs | 32 + .../Helper/WtmMiddleware.cs | 67 + .../WalkingTec.Mvvm.Mvc/IBaseController.cs | 30 + .../WalkingTec.Mvvm.Mvc/Model/Menu.cs | 63 + .../WalkingTec.Mvvm.Mvc/Program.cs | 14 + .../Properties/launchSettings.json | 27 + .../UEditor/UEditorConfigJson.cs | 57 + .../Views/_CodeGen/Gen.cshtml | 196 ++ .../Views/_CodeGen/Index.cshtml | 62 + .../Views/_CodeGen/Preview.cshtml | 12 + .../Views/_CodeGen/SetField.cshtml | 55 + .../Views/_Framework/Selector.cshtml | 108 + .../Views/_ViewImports.cshtml | 2 + .../WalkingTec.Mvvm.Mvc.csproj | 74 + .../WalkingTec.Mvvm.Mvc/_CodeGenController.cs | 136 + .../_FrameworkController.cs | 867 ++++++ .../WalkingTec.Mvvm.Mvc/_SetupController.cs | 34 + .../WalkingTec.Mvvm.Mvc/echarts.common.min.js | 22 + .../WalkingTec.Mvvm.Mvc/framework_layui.js | 1355 ++++++++ .../Abstraction/BaseButton.cs | 136 + .../Abstraction/BaseElementTag.cs | 253 ++ .../Abstraction/BaseFieldTag.cs | 184 ++ .../ButtonTagHelper.cs | 16 + .../CardTagHelper.cs | 63 + .../Chart/ChartTagHelper.cs | 175 ++ .../CodeTagHelper.cs | 29 + .../Common/LayuiUIService.cs | 192 ++ .../ContainerTagHelper.cs | 31 + .../DataTableTagHelper.cs | 1015 ++++++ .../Enums/DataTableSizeEnum.cs | 17 + .../Enums/DataTableSkinEnum.cs | 21 + .../Enums/HttpMethodEnum.cs | 17 + .../Form/CheckBoxTagHelper.cs | 223 ++ .../Form/CloseButtonTagHelper.cs | 21 + .../Form/ColorPicker.cs | 93 + .../Form/ComboBoxTagHelper.cs | 282 ++ .../Form/DateTimeTagHelper.cs | 298 ++ .../Form/DisplayTagHelper.cs | 172 ++ .../Form/DownloadTemplateButtonTagHelper.cs | 26 + .../Form/FieldSetTagHelper.cs | 34 + .../Form/FormTagHelper.cs | 221 ++ .../Form/HiddenTagHelper.cs | 141 + .../Form/MultiUploadTagHelper.cs | 333 ++ .../Form/RadioTagHelper.cs | 179 ++ .../Form/ResetButtonTagHelper.cs | 20 + .../Form/RichTextBox.cs | 97 + .../Form/SearchPanelTagHelper.cs | 275 ++ .../Form/SelectorTagHelper.cs | 382 +++ .../Form/SliderTagHelper.cs | 193 ++ .../Form/SubmitButtonTagHelper.cs | 61 + .../Form/SwitchTagHelper.cs | 76 + .../Form/TextAreaTagHelper.cs | 35 + .../Form/TextBoxTagHelper.cs | 77 + .../Form/TransferTagHelper.cs | 215 ++ .../Form/UEditorTagHelper.cs | 78 + .../Form/UploadTagHelper.cs | 330 ++ .../ImageTagHelper.cs | 47 + .../LayuiServiceCollectionExtensions.cs | 18 + .../LinkButtonTagHelper.cs | 133 + .../Models/LayuiColumn.cs | 166 + .../Models/LayuiTreeItem.cs | 31 + .../PanelTagHelper.cs | 74 + .../Program.cs | 13 + .../QuoteTagHelper.cs | 25 + .../RowTagHelper.cs | 57 + .../TabTagHelper.cs | 98 + .../TreeContainerTagHelper.cs | 218 ++ .../TreeTagHelper.cs | 230 ++ .../WalkingTec.Mvvm.TagHelpers.LayUI.csproj | 21 + 287 files changed, 42372 insertions(+), 8 deletions(-) create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/ActionDescriptionAttribute.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/AllRightsAttribute.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/DebugOnlyAttribute.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/FixConnectionAttribute.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/MiddleTableAttribute.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/NoLogAttribute.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/PublicAttribute.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/ReInitAttribute.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/ValidateFormItemOnlyAttribute.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BaseBatchVM.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BaseCRUDVM.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BaseImportVM.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BasePagedListVM.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BaseSearcher.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BaseTemplateVM.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BaseVM.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/CS.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/Configs.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/CookieOptions.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/Cors.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/DFS.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/DFSTracker.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/DefaultConfigConsts.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/Domain.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/FileUploadOptions.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/JwtOptions.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/KV.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/UEditorOptions.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/UIOptions.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/CoreProgram.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/DataContext.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Enums.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Exceptions/NullOrEmptyStringException.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/ConfigExtension.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/DCExtension.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/DistinctExtension.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/ITreeDataExtension.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/ListExtension.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/ListVMExtension.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/LoginUserInfoExtension.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/PagedListExtension.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/SystemExtensions/DateTimeHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/SystemExtensions/DistributedCacheExtensions.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/SystemExtensions/EnumExtension.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/SystemExtensions/StringExtension.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/SystemExtensions/SystemExtension.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/SystemExtensions/TypeExtension.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/GlobalConstants.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/GlobalData.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/GlobalServices.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/GridAction.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/GridActionExtension.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/GridActionExtension`Old.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/GridColumn.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/GridColumnExtension`Old.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/GridHeaderExtension.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/IGridColumn.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Helper/EntityHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Helper/IServiceExtension.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Helper/LogDebug.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Helper/LogTrace.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Helper/PropertyHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/IBasePagedListVM.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/IBaseVM.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/IDataContext.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ISessionService.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/IUIService.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Implement/DefaultUIService.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/BodyConverter.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/BoolStringConverter.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/DateRangeConverter.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/DateTimeConverter.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/DynamicDataConverter.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/PocoConverter.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/RawStringConverter.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/StringConverter.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/StringIgnoreLTGTConverter.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/TypeConverter.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/JsonResultModel.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/LoginUserInfo.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/MSD.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/ActionLog.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/BasePoco.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/DataPrivilege.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FileAttachment.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FrameworkGroup.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FrameworkMenu.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FrameworkRole.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FrameworkUser.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FrameworkUserGroup.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FrameworkUserRole.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FunctionPrivilege.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/ISearcher.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/ISubFile.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/IWtmFile.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/PersistPoco.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/PersistedGrant.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/TopBasePoco.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/TrackingObj.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/TreePoco.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/AuthConstants.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ChartData.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ClaimComparer.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ColumnFormatInfo.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/CommonEqualityComparer.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/DataPrivilegeInfo.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/DateRange.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/DuplicateInfo.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/DynamicData.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ExcelPropety.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ExpressionVisitors.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ExtraClass.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/FileHandlers/IWtmFileHandler.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmDataBaseFileHandler.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmFileHandlerBase.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmFileProvider.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmLocalFileHandler.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmOssFileHandler.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleAction.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleArea.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleDataPri.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleFunctionPri.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleGroup.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleLog.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleMenu.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleModule.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleRole.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleUserInfo.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ListItem.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/NugetInfo.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/SupportedGroupMapping.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/TypeComparer.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/WTMLogger.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/WebProxy.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/WtmLocalizationOption.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Utils.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/WTMContext.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Core/WalkingTec.Mvvm.Core.csproj create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Auth/Attribute/AuthorizeCookieAttribute.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Auth/Attribute/AuthorizeJwtAttribute.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Auth/Attribute/AuthorizeJwtWithCookieAttribute.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Auth/JwtAuth/ITokenService.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Auth/JwtAuth/Token.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Auth/JwtAuth/TokenService.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/BaseApiController.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/BaseController.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Binders/DateRangeBinder.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Binders/EnumBinder.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Binders/StringBinderProvider.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Binders/StringIgnoreLTGTBinder.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Binders/StringNeedLTGTAttribute.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/CodeGenListVM.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/CodeGenVM.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Filters/DataContextFilter.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Filters/FrameworkFilter.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Filters/PrivilegeFilter.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Filters/SwaggerFilter.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/ApiTest.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/ApiTestTopPoco.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/BatchVM.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/ControllerTest.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/ControllerTestTopPoco.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/CrudVM.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/HeaderFormat.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/ImportVM.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/ListVM.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/BatchDeleteView.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/BatchEditView.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/Controller.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/CreateView.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/DeleteView.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/DetailsView.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/EditView.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/ImportView.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/ListView.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Searcher.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Blazor/Controller.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Blazor/Create.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Blazor/Details.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Blazor/Edit.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Blazor/Import.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Blazor/Index.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Controller.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/React/index.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/React/store/index.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/React/style.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/React/views/action.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/React/views/forms.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/React/views/models.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/React/views/other.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/React/views/search.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/React/views/table.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Vue/config.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Vue/index.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Vue/store/api.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Vue/store/index.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Vue/views/dialog-form.txt create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Helper/ActionDescriptionExtension.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Helper/ActionExecutingContextExtension.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Helper/FResult.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Helper/FResultExtension.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Helper/FileExtension.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Helper/FrameworkServiceExtension.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Helper/HttpContextExtention.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Helper/IconFontsHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Helper/ModelStateExtension.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Helper/ModelStateServiceProvider.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Helper/MvcOptionExtension.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Helper/SessionExtension.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Helper/SessionServiceProvider.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Helper/WtmContextOption.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Helper/WtmMiddleware.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/IBaseController.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Model/Menu.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Program.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Properties/launchSettings.json create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/UEditor/UEditorConfigJson.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Views/_CodeGen/Gen.cshtml create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Views/_CodeGen/Index.cshtml create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Views/_CodeGen/Preview.cshtml create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Views/_CodeGen/SetField.cshtml create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Views/_Framework/Selector.cshtml create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Views/_ViewImports.cshtml create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/WalkingTec.Mvvm.Mvc.csproj create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/_CodeGenController.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/_FrameworkController.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/_SetupController.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/echarts.common.min.js create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/framework_layui.js create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Abstraction/BaseButton.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Abstraction/BaseElementTag.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Abstraction/BaseFieldTag.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/ButtonTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/CardTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Chart/ChartTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/CodeTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Common/LayuiUIService.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/ContainerTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/DataTableTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Enums/DataTableSizeEnum.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Enums/DataTableSkinEnum.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Enums/HttpMethodEnum.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Form/CheckBoxTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Form/CloseButtonTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Form/ColorPicker.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Form/ComboBoxTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Form/DateTimeTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Form/DisplayTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Form/DownloadTemplateButtonTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Form/FieldSetTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Form/FormTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Form/HiddenTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Form/MultiUploadTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Form/RadioTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Form/ResetButtonTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Form/RichTextBox.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Form/SearchPanelTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Form/SelectorTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Form/SliderTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Form/SubmitButtonTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Form/SwitchTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Form/TextAreaTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Form/TextBoxTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Form/TransferTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Form/UEditorTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Form/UploadTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/ImageTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/LayuiServiceCollectionExtensions.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/LinkButtonTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Models/LayuiColumn.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Models/LayuiTreeItem.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/PanelTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/Program.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/QuoteTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/RowTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/TabTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/TreeContainerTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/TreeTagHelper.cs create mode 100644 WalkingTec.Mvvm/WalkingTec.Mvvm.TagHelpers.LayUI/WalkingTec.Mvvm.TagHelpers.LayUI.csproj diff --git a/.vs/IoTGateway/DesignTimeBuild/.dtbcache.v2 b/.vs/IoTGateway/DesignTimeBuild/.dtbcache.v2 index ef23d53b95aefb42e063bf7f5dab2e2648fcd444..9c0d62774fe322daca6ecf0ebbf96652fb26304f 100644 GIT binary patch literal 394265 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 Nl;%<8+}{9`1^|3pZaM$} delta 33 pcmcaAc$$}ye|Sub;1 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 delta 12100 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 CO6t%6 diff --git a/IoTGateway.Model/IoTGateway.Model.csproj b/IoTGateway.Model/IoTGateway.Model.csproj index 1221e71..0623834 100644 --- a/IoTGateway.Model/IoTGateway.Model.csproj +++ b/IoTGateway.Model/IoTGateway.Model.csproj @@ -6,13 +6,9 @@ - - - - - + diff --git a/IoTGateway.sln b/IoTGateway.sln index fdd9b7c..9f03442 100644 --- a/IoTGateway.sln +++ b/IoTGateway.sln @@ -21,6 +21,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Drivers", "Drivers", "{52D9 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DriverModbusTCP", "Plugins\Drivers\DriverModbusTCP\DriverModbusTCP.csproj", "{7B432FC9-57E6-44BF-B8A7-2A1FB31D6ADD}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WalkingTec.Mvvm", "WalkingTec.Mvvm", "{98B1C9F0-028C-48D8-8148-54B69CCA4590}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WalkingTec.Mvvm.Core", "WalkingTec.Mvvm\WalkingTec.Mvvm.Core\WalkingTec.Mvvm.Core.csproj", "{C2672620-8E65-486C-B967-C4C673F8DA0F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WalkingTec.Mvvm.Mvc", "WalkingTec.Mvvm\WalkingTec.Mvvm.Mvc\WalkingTec.Mvvm.Mvc.csproj", "{B370F699-965B-4D86-93B1-0F022C95B5C9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WalkingTec.Mvvm.TagHelpers.LayUI", "WalkingTec.Mvvm\WalkingTec.Mvvm.TagHelpers.LayUI\WalkingTec.Mvvm.TagHelpers.LayUI.csproj", "{81CBFD0E-1D89-440A-8CC3-E32672504FF4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -55,6 +63,18 @@ Global {7B432FC9-57E6-44BF-B8A7-2A1FB31D6ADD}.Debug|Any CPU.Build.0 = Debug|Any CPU {7B432FC9-57E6-44BF-B8A7-2A1FB31D6ADD}.Release|Any CPU.ActiveCfg = Release|Any CPU {7B432FC9-57E6-44BF-B8A7-2A1FB31D6ADD}.Release|Any CPU.Build.0 = Release|Any CPU + {C2672620-8E65-486C-B967-C4C673F8DA0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2672620-8E65-486C-B967-C4C673F8DA0F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2672620-8E65-486C-B967-C4C673F8DA0F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2672620-8E65-486C-B967-C4C673F8DA0F}.Release|Any CPU.Build.0 = Release|Any CPU + {B370F699-965B-4D86-93B1-0F022C95B5C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B370F699-965B-4D86-93B1-0F022C95B5C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B370F699-965B-4D86-93B1-0F022C95B5C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B370F699-965B-4D86-93B1-0F022C95B5C9}.Release|Any CPU.Build.0 = Release|Any CPU + {81CBFD0E-1D89-440A-8CC3-E32672504FF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {81CBFD0E-1D89-440A-8CC3-E32672504FF4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {81CBFD0E-1D89-440A-8CC3-E32672504FF4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {81CBFD0E-1D89-440A-8CC3-E32672504FF4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -64,6 +84,9 @@ Global {E5F79995-AB61-41F4-820D-BA39967B406B} = {FBED048F-7AB9-4348-AD56-F9BF4D9E3A55} {52D96C24-2F2F-49B5-9F29-00414DEA41D8} = {FBED048F-7AB9-4348-AD56-F9BF4D9E3A55} {7B432FC9-57E6-44BF-B8A7-2A1FB31D6ADD} = {52D96C24-2F2F-49B5-9F29-00414DEA41D8} + {C2672620-8E65-486C-B967-C4C673F8DA0F} = {98B1C9F0-028C-48D8-8148-54B69CCA4590} + {B370F699-965B-4D86-93B1-0F022C95B5C9} = {98B1C9F0-028C-48D8-8148-54B69CCA4590} + {81CBFD0E-1D89-440A-8CC3-E32672504FF4} = {98B1C9F0-028C-48D8-8148-54B69CCA4590} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1F219808-E6E8-4C1D-B846-41F2F7CF20FA} diff --git a/IoTGateway/IoTGateway.csproj b/IoTGateway/IoTGateway.csproj index 4a8eca7..740db28 100644 --- a/IoTGateway/IoTGateway.csproj +++ b/IoTGateway/IoTGateway.csproj @@ -16,8 +16,6 @@ - - @@ -25,6 +23,8 @@ + + diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/ActionDescriptionAttribute.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/ActionDescriptionAttribute.cs new file mode 100644 index 0000000..ee3a209 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/ActionDescriptionAttribute.cs @@ -0,0 +1,46 @@ +using System; +using System.Linq; +using Microsoft.Extensions.Localization; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// 标记Controller和Action的描述 + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)] + public class ActionDescriptionAttribute : Attribute + { + /// + /// 描述 + /// + public string Description { get; set; } + public string ClassFullName { get; set; } + public bool IsPage { get; set; } + public IStringLocalizer _localizer { get; set; } + /// + /// 新建一个描述 + /// + public ActionDescriptionAttribute(string desc) + { + this.Description = desc; + } + + public ActionDescriptionAttribute(string desc, string classFullName) + { + this.Description = desc; + this.ClassFullName = classFullName; + } + + public ActionDescriptionAttribute(string desc, string classFullName, bool ispage) + { + this.Description = desc; + this.ClassFullName = classFullName; + this.IsPage = ispage; + } + + public void SetLoccalizer(Type controllertype) + { + _localizer = Core.CoreProgram._localizer; + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/AllRightsAttribute.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/AllRightsAttribute.cs new file mode 100644 index 0000000..52d5a9a --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/AllRightsAttribute.cs @@ -0,0 +1,12 @@ +using System; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// 标记Controller或Action不受权限控制,只要登录任何人都可访问 + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)] + public class AllRightsAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/DebugOnlyAttribute.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/DebugOnlyAttribute.cs new file mode 100644 index 0000000..21083f8 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/DebugOnlyAttribute.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace WalkingTec.Mvvm.Core +{ + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)] + public class DebugOnlyAttribute : Attribute + { + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/FixConnectionAttribute.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/FixConnectionAttribute.cs new file mode 100644 index 0000000..25c13c5 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/FixConnectionAttribute.cs @@ -0,0 +1,50 @@ +using System; + +namespace WalkingTec.Mvvm.Core +{ + public enum DBOperationEnum { Default, Read, Write } + /// + /// 标记Controller或Action使用固定的连接字符串,不受其他设定控制 + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)] + public class FixConnectionAttribute : Attribute + { + /// + /// 连接字符串名称 + /// + public string CsName { get; set; } + + public DBTypeEnum? DbType { get; set; } + + /// + /// 操作类型,读或写 + /// + public DBOperationEnum Operation { get; set; } + + /// + /// 新建固定连接字符串标记 + /// + /// Operation + /// the key of the ConnectionString in appsettings + public FixConnectionAttribute(DBOperationEnum Operation = DBOperationEnum.Default, string CsName = "") + { + this.CsName = CsName; + this.Operation = Operation; + this.DbType = null; + } + + /// + /// 操作类型,读或写 + /// + /// the database type, if t is Default, the value in appsettings will be used + /// Operation + /// the key of the ConnectionString in appsettings + public FixConnectionAttribute(DBTypeEnum DbType, DBOperationEnum Operation = DBOperationEnum.Default, string CsName = "") + { + this.CsName = CsName; + this.Operation = Operation; + this.DbType = DbType; + } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/MiddleTableAttribute.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/MiddleTableAttribute.cs new file mode 100644 index 0000000..71ec40b --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/MiddleTableAttribute.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace WalkingTec.Mvvm.Core.Attributes +{ + + /// + /// 标记某个Model是一个多对多的中间表 + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class MiddleTableAttribute : Attribute + { + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/NoLogAttribute.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/NoLogAttribute.cs new file mode 100644 index 0000000..8e9aa17 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/NoLogAttribute.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace WalkingTec.Mvvm.Core +{ + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)] + public class NoLogAttribute : Attribute + { + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/PublicAttribute.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/PublicAttribute.cs new file mode 100644 index 0000000..fecda75 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/PublicAttribute.cs @@ -0,0 +1,14 @@ +using System; +using Microsoft.AspNetCore.Authorization; + +namespace WalkingTec.Mvvm.Core +{ + //[Obsolete("已废弃,预计v3.0版本及v2.10版本开始将删除")] + /// + /// 标记Action返回的为公共页面,跳过权限验证,不需要登录即可访问 + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)] + public class PublicAttribute : Attribute, IAllowAnonymous + { + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/ReInitAttribute.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/ReInitAttribute.cs new file mode 100644 index 0000000..16f1a1c --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/ReInitAttribute.cs @@ -0,0 +1,27 @@ +using System; + +namespace WalkingTec.Mvvm.Core +{ + public enum ReInitModes { FAILEDONLY, SUCCESSONLY, ALWAYS } + + /// + /// 标记VM中的ReInit方法是在提交错误时触发,提交成功时触发,或是都触发。这是为了一些特殊逻辑的VM设计的 + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class ReInitAttribute : Attribute + { + /// + /// 触发模式 + /// + public ReInitModes ReInitMode { get; set; } + + /// + /// 新建触发标记 + /// + /// 触发模式 + public ReInitAttribute(ReInitModes mode) + { + this.ReInitMode = mode; + } + } +} \ No newline at end of file diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/ValidateFormItemOnlyAttribute.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/ValidateFormItemOnlyAttribute.cs new file mode 100644 index 0000000..ef48eec --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Attributes/ValidateFormItemOnlyAttribute.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace WalkingTec.Mvvm.Core +{ + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)] + public class ValidateFormItemOnlyAttribute : Attribute + { + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BaseBatchVM.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BaseBatchVM.cs new file mode 100644 index 0000000..66c2fc5 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BaseBatchVM.cs @@ -0,0 +1,419 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; +using System.Text.Json.Serialization; +using WalkingTec.Mvvm.Core.Extensions; +using WalkingTec.Mvvm.Core.Support.FileHandlers; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// 批量操作VM接口 + /// + /// 批量修改的VM + public interface IBaseBatchVM + where TEditModel : BaseVM + { + /// + /// 批量修改的VM + /// + TEditModel LinkedVM { get; } + + /// + /// 批量列表VM + /// + IBasePagedListVM ListVM { get; } + + /// + /// 列表数据的Id数组 + /// + //IEnumerable Ids { get; set; } + string[] Ids { get; set; } + + /// + /// 批量操作的错误 + /// + Dictionary ErrorMessage { get; set; } + } + + /// + /// 批量操作的基础VM,所有批量操作的VM应继承本类 + /// + /// 批量修改的VM + /// 批量列表VM + public class BaseBatchVM : BaseVM, IBaseBatchVM where TModel : TopBasePoco,new() where TLinkModel : BaseVM + { + /// + /// 批量修改的VM + /// + + public TLinkModel LinkedVM { get; set; } + + /// + /// 批量列表VM + /// + [JsonIgnore] + public IBasePagedListVM ListVM { get; set; } + + /// + /// 批量操作的错误 + /// + [JsonIgnore] + public Dictionary ErrorMessage { get; set; } + + /// + /// 列表数据的Id数组 + /// + public string[] Ids { get; set; } + + /// + /// 构造函数 + /// + public BaseBatchVM() + { + //this.Ids = new List(); + ErrorMessage = new Dictionary(); + } + + /// + /// 添加错误信息 + /// + /// 错误 + /// 数据Id + protected void SetExceptionMessage(Exception e, string id) + { + if (id != null) + { + ErrorMessage.Add(id, e.Message); + } + } + + /// + /// 检查是否可以删除,当进行批量删除操作时会调用本函数。子类如果有特殊判断应重载本函数 + /// + /// 数据Id + /// 错误信息 + /// true代表可以删除,false代表不能删除 + protected virtual bool CheckIfCanDelete(object id, out string errorMessage) + { + errorMessage = null; + return true; + } + + /// + /// 批量删除,默认对Ids中包含的主键的数据进行删除。子类如果有特殊判断应重载本函数 + /// + /// true代表成功,false代表失败 + public virtual bool DoBatchDelete() + { + bool rv = true; + //循环所有数据Id + List idsData = Ids.ToList(); + var modelType = typeof(TModel); + var pros = modelType.GetAllProperties(); + //如果包含附件,则先删除附件 + List fileids = new List(); + var fa = pros.Where(x => x.PropertyType == typeof(FileAttachment) || typeof(TopBasePoco).IsAssignableFrom(x.PropertyType)).ToList(); + var isPersist =typeof(IPersistPoco).IsAssignableFrom(modelType); + var isBasePoco = typeof(IBasePoco).IsAssignableFrom(modelType); + var query = DC.Set().AsQueryable(); + var fas = pros.Where(x => typeof(IEnumerable).IsAssignableFrom(x.PropertyType)).ToList(); + foreach (var f in fas) + { + query = query.Include(f.Name); + } + query = query.AsNoTracking().CheckIDs(idsData); + var entityList = query.ToList(); + for (int i = 0; i < entityList.Count; i++) + { + string checkErro = null; + //检查是否可以删除,如不能删除则直接跳过 + if (CheckIfCanDelete(idsData[i], out checkErro) == false) + { + ErrorMessage.Add(idsData[i], checkErro); + rv = false; + break; + } + //进行删除 + try + { + var Entity = entityList[i]; + if (isPersist) + { + (Entity as IPersistPoco).IsValid = false; + DC.UpdateProperty(Entity, "IsValid"); + if (isBasePoco) + { + (Entity as IBasePoco).UpdateTime = DateTime.Now; + (Entity as IBasePoco).UpdateBy = LoginUserInfo.ITCode; + DC.UpdateProperty(Entity, "UpdateTime"); + DC.UpdateProperty(Entity, "UpdateBy"); + } + } + else + { + + foreach (var f in fa) + { + if (f.PropertyType == typeof(FileAttachment)) + { + string fidfield = DC.GetFKName2(modelType, f.Name); + var fidpro = pros.Where(x => x.Name == fidfield).FirstOrDefault(); + var idresult = fidpro.GetValue(Entity); + if(idresult != null) + { + Guid fid = Guid.Empty; + if(Guid.TryParse(idresult.ToString(), out fid) == true) + { + fileids.Add(fid); + } + } + } + f.SetValue(Entity, null); + } + + foreach (var f in fas) + { + var subs = f.GetValue(Entity) as IEnumerable; + if (subs != null) + { + foreach (var sub in subs) + { + fileids.Add(sub.FileId); + } + f.SetValue(Entity, null); + } + else + { + + } + } + if (typeof(TModel) != typeof(FileAttachment)) + { + foreach (var pro in pros) + { + if (pro.PropertyType.GetTypeInfo().IsSubclassOf(typeof(TopBasePoco))) + { + pro.SetValue(Entity, null); + } + } + } + DC.DeleteEntity(Entity); + } + } + catch (Exception e) + { + SetExceptionMessage(e, idsData[i]); + rv = false; + } + } + //进行数据库的删除操作 + if (rv == true) + { + try + { + DC.SaveChanges(); + var fp = Wtm.ServiceProvider.GetRequiredService(); + foreach (var item in fileids) + { + fp.DeleteFile(item.ToString(), DC.ReCreate()); + } + } + catch (Exception e) + { + SetExceptionMessage(e, null); + rv = false; + } + } + //如果失败,添加错误信息 + if (rv == false) + { + if (ErrorMessage.Count > 0) + { + foreach (var id in idsData) + { + if (!ErrorMessage.ContainsKey(id)) + { + ErrorMessage.Add(id, CoreProgram._localizer?["Sys.Rollback"]); + } + } + } + ListVM?.DoSearch(); + if (ListVM != null) + { + foreach (var item in ListVM?.GetEntityList()) + { + item.BatchError = ErrorMessage.Where(x => x.Key == item.GetID().ToString()).Select(x => x.Value).FirstOrDefault(); + } + } + MSD.AddModelError("", CoreProgram._localizer?["Sys.DataCannotDelete"]); + } + return rv; + } + + + /// + /// 批量修改,默认对Ids中包含的数据进行修改,子类如果有特殊判断应重载本函数 + /// + /// true代表成功,false代表失败 + public virtual bool DoBatchEdit() + { + //获取批量修改VM的所有属性 + var pros = LinkedVM.GetType().GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.DeclaredOnly); + bool rv = true; + List idsData = Ids.ToList(); + string currentvmname = this.GetType().Name; + Type vmtype = null; + //找到对应的BaseCRUDVM,并初始化 + if (currentvmname.ToLower().Contains("apibatchvm")) + { + vmtype = this.GetType().Assembly.GetExportedTypes().Where(x => x.IsSubclassOf(typeof(BaseCRUDVM)) && x.Name.ToLower().Contains("apivm") == true).FirstOrDefault(); + } + else + { + vmtype = this.GetType().Assembly.GetExportedTypes().Where(x => x.IsSubclassOf(typeof(BaseCRUDVM)) && x.Name.ToLower().Contains("apivm") == false).FirstOrDefault(); + } + IBaseCRUDVM vm = null; + if (vmtype != null) + { + vm = vmtype.GetConstructor(System.Type.EmptyTypes).Invoke(null) as IBaseCRUDVM; + vm.CopyContext(this); + } + var entityList = DC.Set().CheckIDs(idsData).ToList(); + //循环所有数据 + for (int i = 0; i < entityList.Count; i++) + { + try + { + //如果找不到对应数据,则输出错误 + TModel entity = entityList[i]; + if (vm != null) + { + vm.SetEntity(entity); + } + if (entity == null) + { + ErrorMessage.Add(idsData[i], CoreProgram._localizer?["Sys.DataNotExist"]); + rv = false; + break; + } + //如果能找到,则循环LinkedVM中的属性,给entity中同名属性赋值 + foreach (var pro in pros) + { + var proToSet = entity.GetType().GetSingleProperty(pro.Name); + var val = FC.ContainsKey("LinkedVM." + pro.Name) ? FC["LinkedVM." + pro.Name] : null; + if(val == null && FC.ContainsKey("LinkedVM." + pro.Name + "[]")) + { + val = FC["LinkedVM." + pro.Name + "[]"]; + } + if (proToSet != null && val != null) + { + var hasvalue = true; + if ( val is StringValues sv && StringValues.IsNullOrEmpty(sv) == true) + { + hasvalue = false; + } + if (hasvalue) + { + proToSet.SetValue(entity, pro.GetValue(LinkedVM)); + } + } + } + + //调用controller方法验证model + //try + //{ + // Controller.GetType().GetMethod("RedoValidation").Invoke(Controller, new object[] { entity }); + //} + //catch { } + //如果有对应的BaseCRUDVM则使用其进行数据验证 + if (vm != null) + { + vm.Validate(); + var errors = vm.MSD; + if (errors != null && errors.Count > 0) + { + var error = ""; + foreach (var key in errors.Keys) + { + if (errors[key].Count > 0) + { + error += errors[key].Select(x => x.ErrorMessage).ToSepratedString(); + } + } + if (error != "") + { + ErrorMessage.Add(idsData[i], error); + rv = false; + break; + } + } + } + if (typeof(IBasePoco).IsAssignableFrom( typeof(TModel))) + { + IBasePoco ent = entity as IBasePoco; + if (ent.UpdateTime == null) + { + ent.UpdateTime = DateTime.Now; + } + if (string.IsNullOrEmpty(ent.UpdateBy)) + { + ent.UpdateBy = LoginUserInfo?.ITCode; + } + } + DC.UpdateEntity(entity); + } + catch (Exception e) + { + SetExceptionMessage(e, idsData[i]); + rv = false; + } + } + //进行数据库的修改操作 + if (rv == true) + { + try + { + DC.SaveChanges(); + } + catch (Exception e) + { + SetExceptionMessage(e, null); + rv = false; + } + } + + //如果有错误,输出错误信息 + if (rv == false) + { + if (ErrorMessage.Count > 0) + { + foreach (var id in idsData) + { + if (!ErrorMessage.ContainsKey(id)) + { + ErrorMessage.Add(id, CoreProgram._localizer?["Sys.Rollback"]); + } + } + } + RefreshErrorList(); + } + return rv; + } + + protected void RefreshErrorList() + { + ListVM.DoSearch(); + foreach (var item in ListVM.GetEntityList()) + { + item.BatchError = ErrorMessage.Where(x => x.Key == item.GetID().ToString()).Select(x => x.Value).FirstOrDefault(); + } + + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BaseCRUDVM.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BaseCRUDVM.cs new file mode 100644 index 0000000..d74eb23 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BaseCRUDVM.cs @@ -0,0 +1,1054 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using WalkingTec.Mvvm.Core.Extensions; +using WalkingTec.Mvvm.Core.Support.FileHandlers; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// 单表增删改查VM的接口 + /// + /// 继承TopBasePoco的类 + public interface IBaseCRUDVM where T : TopBasePoco, new() + { + T Entity { get; } + /// + /// 根据主键Id获取Entity + /// + /// 主键Id + void SetEntityById(object id); + + /// + /// 设置Entity + /// + /// 要设定的TopBasePoco + void SetEntity(object entity); + + /// + /// 添加 + /// + void DoAdd(); + + Task DoAddAsync(); + + /// + /// 修改 + /// + void DoEdit(bool updateAllFields); + Task DoEditAsync(bool updateAllFields); + + /// + /// 删除,对于TopBasePoco进行物理删除,对于PersistPoco把IsValid修改为false + /// + void DoDelete(); + Task DoDeleteAsync(); + + /// + /// 彻底删除,对PersistPoco进行物理删除 + /// + void DoRealDelete(); + Task DoRealDeleteAsync(); + + /// + /// 将源VM的上数据库上下文,Session,登录用户信息,模型状态信息,缓存信息等内容复制到本VM中 + /// + /// 复制的源 + void CopyContext(BaseVM vm); + + /// + /// 是否跳过基类的唯一性验证,批量导入的时候唯一性验证会由存储过程完成,不需要单独调用本类的验证方法 + /// + bool ByPassBaseValidation { get; set; } + + void Validate(); + IModelStateService MSD { get; } + } + + /// + /// 单表增删改查基类,所有单表操作的VM应该继承这个基类 + /// + /// 继承TopBasePoco的类 + public class BaseCRUDVM : BaseVM, IBaseCRUDVM where TModel : TopBasePoco, new() + { + public TModel Entity { get; set; } + [JsonIgnore] + public bool ByPassBaseValidation { get; set; } + + //保存读取时Include的内容 + private List>> _toInclude { get; set; } + + /// + /// 构造函数 + /// + public BaseCRUDVM() + { + //初始化Entity + var ctor = typeof(TModel).GetConstructor(Type.EmptyTypes); + Entity = ctor.Invoke(null) as TModel; + //初始化VM中所有List<>的类 + //var lists = typeof(TModel).GetAllProperties().Where(x => x.PropertyType.IsGeneric(typeof(List<>))); + //foreach (var li in lists) + //{ + // var gs = li.PropertyType.GetGenericArguments(); + // var newObj = Activator.CreateInstance(typeof(List<>).MakeGenericType(gs[0])); + // li.SetValue(Entity, newObj, null); + //} + } + + public IQueryable GetBaseQuery() + { + return DC.Set(); + } + /// + /// 设定添加和修改时对于重复数据的判断,子类进行相关操作时应重载这个函数 + /// + /// 唯一性属性 + public virtual DuplicatedInfo SetDuplicatedCheck() + { + return null; + } + + /// + /// 设定读取是Include的内容 + /// + /// 需要关联的类 + public void SetInclude(params Expression>[] exps) + { + _toInclude = _toInclude ?? new List>>(); + _toInclude.AddRange(exps); + } + + /// + /// 根据主键Id设定Entity + /// + /// 主键Id + public void SetEntityById(object id) + { + this.Entity = GetById(id); + } + + /// + /// 设置Entity + /// + /// 要设定的TopBasePoco + public void SetEntity(object entity) + { + this.Entity = entity as TModel; + } + + /// + /// 根据主键获取Entity + /// + /// 主键Id + /// Entity + protected virtual TModel GetById(object Id) + { + TModel rv = null; + //建立基础查询 + var query = DC.Set().AsQueryable(); + //循环添加其他设定的Include + if (_toInclude != null) + { + foreach (var item in _toInclude) + { + query = query.Include(item); + } + } + if (typeof(IPersistPoco).IsAssignableFrom(typeof(TModel))) + { + var mod = new IsValidModifier(); + var newExp = mod.Modify(query.Expression); + query = query.Provider.CreateQuery(newExp) as IOrderedQueryable; + } + + //获取数据 + rv = query.CheckID(Id).AsNoTracking().SingleOrDefault(); + if (rv == null) + { + throw new Exception("数据不存在"); + } + //如果TopBasePoco有关联的附件,则自动Include 附件名称 + var pros = typeof(TModel).GetAllProperties(); + var fa = pros.Where(x => x.PropertyType == typeof(FileAttachment)).ToList(); + foreach (var f in fa) + { + var fname = DC.GetFKName2(f.Name); + var fid = typeof(TModel).GetSingleProperty(fname).GetValue(rv); + if (fid != null && Wtm.ServiceProvider != null) + { + var fp = Wtm.ServiceProvider.GetRequiredService(); + var file = fp.GetFile(fid?.ToString(), false, DC); + rv.SetPropertyValue(f.Name, file); + } + } + return rv; + } + + /// + /// 添加,进行默认的添加操作。子类如有自定义操作应重载本函数 + /// + public virtual void DoAdd() + { + DoAddPrepare(); + //删除不需要的附件 + if (DeletedFileIds != null && DeletedFileIds.Count > 0 && Wtm.ServiceProvider != null) + { + var fp = Wtm.ServiceProvider.GetRequiredService(); + + foreach (var item in DeletedFileIds) + { + fp.DeleteFile(item.ToString(), DC); + } + } + DC.SaveChanges(); + } + + public virtual async Task DoAddAsync() + { + DoAddPrepare(); + //删除不需要的附件 + if (DeletedFileIds != null && DeletedFileIds.Count > 0 && Wtm.ServiceProvider != null) + { + var fp = Wtm.ServiceProvider.GetRequiredService(); + + foreach (var item in DeletedFileIds) + { + fp.DeleteFile(item.ToString(), DC.ReCreate()); + } + } + await DC.SaveChangesAsync(); + } + + private void DoAddPrepare() + { + var pros = typeof(TModel).GetAllProperties(); + //将所有TopBasePoco的属性赋空值,防止添加关联的重复内容 + if (typeof(TModel) != typeof(FileAttachment)) + { + foreach (var pro in pros) + { + if (pro.PropertyType.GetTypeInfo().IsSubclassOf(typeof(TopBasePoco))) + { + pro.SetValue(Entity, null); + } + } + } + //自动设定添加日期和添加人 + if (typeof(IBasePoco).IsAssignableFrom(typeof(TModel))) + { + IBasePoco ent = Entity as IBasePoco; + if (ent.CreateTime == null) + { + ent.CreateTime = DateTime.Now; + } + if (string.IsNullOrEmpty(ent.CreateBy)) + { + ent.CreateBy = LoginUserInfo?.ITCode; + } + } + + if (typeof(IPersistPoco).IsAssignableFrom(typeof(TModel))) + { + (Entity as IPersistPoco).IsValid = true; + } + + #region 更新子表 + foreach (var pro in pros) + { + //找到类型为List的字段 + if (pro.PropertyType.GenericTypeArguments.Count() > 0) + { + //获取xxx的类型 + var ftype = pro.PropertyType.GenericTypeArguments.First(); + //如果xxx继承自TopBasePoco + if (ftype.IsSubclassOf(typeof(TopBasePoco))) + { + //界面传过来的子表数据 + IEnumerable list = pro.GetValue(Entity) as IEnumerable; + if (list != null && list.Count() > 0) + { + string fkname = DC.GetFKName(pro.Name); + var itemPros = ftype.GetAllProperties(); + + bool found = false; + foreach (var newitem in list) + { + foreach (var itempro in itemPros) + { + if (itempro.PropertyType.IsSubclassOf(typeof(TopBasePoco))) + { + itempro.SetValue(newitem, null); + } + if (!string.IsNullOrEmpty(fkname)) + { + if (itempro.Name.ToLower() == fkname.ToLower()) + { + try + { + itempro.SetValue(newitem, Entity.GetID()); + found = true; + } + catch { } + } + } + } + } + //如果没有找到相应的外建字段,则可能是多对多的关系,或者做了特殊的设定,这种情况框架无法支持,直接退出本次循环 + if (found == false) + { + continue; + } + //循环页面传过来的子表数据,自动设定添加日期和添加人 + foreach (var newitem in list) + { + var subtype = newitem.GetType(); + if (typeof(IBasePoco).IsAssignableFrom(subtype)) + { + IBasePoco ent = newitem as IBasePoco; + if (ent.CreateTime == null) + { + ent.CreateTime = DateTime.Now; + } + if (string.IsNullOrEmpty(ent.CreateBy)) + { + ent.CreateBy = LoginUserInfo?.ITCode; + } + } + } + } + } + } + } + #endregion + + + //添加数据 + DC.Set().Add(Entity); + + } + + /// + /// 修改,进行默认的修改操作。子类如有自定义操作应重载本函数 + /// + /// 为true时,框架会更新当前Entity的全部值,为false时,框架会检查Request.Form里的key,只更新表单提交的字段 + public virtual void DoEdit(bool updateAllFields = false) + { + DoEditPrepare(updateAllFields); + + try + { + DC.SaveChanges(); + } + catch + { + MSD.AddModelError(" ", Localizer["Sys.EditFailed"]); + } + //删除不需要的附件 + if (DeletedFileIds != null && DeletedFileIds.Count > 0 && Wtm.ServiceProvider != null) + { + var fp = Wtm.ServiceProvider.GetRequiredService(); + + foreach (var item in DeletedFileIds) + { + fp.DeleteFile(item.ToString(), DC.ReCreate()); + } + } + + } + + public virtual async Task DoEditAsync(bool updateAllFields = false) + { + DoEditPrepare(updateAllFields); + + await DC.SaveChangesAsync(); + //删除不需要的附件 + if (DeletedFileIds != null && DeletedFileIds.Count > 0 && Wtm.ServiceProvider != null) + { + var fp = Wtm.ServiceProvider.GetRequiredService(); + + foreach (var item in DeletedFileIds) + { + fp.DeleteFile(item.ToString(), DC); + } + } + } + + private void DoEditPrepare(bool updateAllFields) + { + if (typeof(IBasePoco).IsAssignableFrom(typeof(TModel))) + { + IBasePoco ent = Entity as IBasePoco; + //if (ent.UpdateTime == null) + //{ + ent.UpdateTime = DateTime.Now; + //} + //if (string.IsNullOrEmpty(ent.UpdateBy)) + //{ + ent.UpdateBy = LoginUserInfo?.ITCode; + //} + } + var pros = typeof(TModel).GetAllProperties(); + pros = pros.Where(x => x.CustomAttributes.Any(y => y.AttributeType == typeof(NotMappedAttribute)) == false).ToList(); + if (typeof(TModel) != typeof(FileAttachment)) + { + foreach (var pro in pros) + { + if (pro.PropertyType.GetTypeInfo().IsSubclassOf(typeof(TopBasePoco))) + { + pro.SetValue(Entity, null); + } + } + } + #region 更新子表 + foreach (var pro in pros) + { + //找到类型为List的字段 + if (pro.PropertyType.GenericTypeArguments.Count() > 0) + { + //获取xxx的类型 + var ftype = pro.PropertyType.GenericTypeArguments.First(); + //如果xxx继承自TopBasePoco + if (ftype.IsSubclassOf(typeof(TopBasePoco))) + { + //界面传过来的子表数据 + + if (pro.GetValue(Entity) is IEnumerable list && list.Count() > 0) + { + //获取外键字段名称 + string fkname = DC.GetFKName(pro.Name); + var itemPros = ftype.GetAllProperties(); + + bool found = false; + foreach (var newitem in list) + { + var subtype = newitem.GetType(); + if (typeof(IBasePoco).IsAssignableFrom(subtype)) + { + IBasePoco ent = newitem as IBasePoco; + if (ent.UpdateTime == null) + { + ent.UpdateTime = DateTime.Now; + } + if (string.IsNullOrEmpty(ent.UpdateBy)) + { + ent.UpdateBy = LoginUserInfo?.ITCode; + } + } + //循环页面传过来的子表数据,将关联到TopBasePoco的字段设为null,并且把外键字段的值设定为主表ID + foreach (var itempro in itemPros) + { + if (itempro.PropertyType.IsSubclassOf(typeof(TopBasePoco))) + { + itempro.SetValue(newitem, null); + } + if (!string.IsNullOrEmpty(fkname)) + { + if (itempro.Name.ToLower() == fkname.ToLower()) + { + itempro.SetValue(newitem, Entity.GetID()); + found = true; + } + } + } + } + //如果没有找到相应的外建字段,则可能是多对多的关系,或者做了特殊的设定,这种情况框架无法支持,直接退出本次循环 + if (found == false) + { + continue; + } + + + TModel _entity = null; + //打开新的数据库联接,获取数据库中的主表和子表数据 + //using (var ndc = DC.CreateNew()) + //{ + _entity = DC.Set().Include(pro.Name).AsNoTracking().CheckID(Entity.GetID()).FirstOrDefault(); + //} + if (_entity == null) + { + MSD.AddModelError(" ", Localizer["Sys.EditFailed"]); + return; + } + //比较子表原数据和新数据的区别 + IEnumerable toadd = null; + IEnumerable toremove = null; + IEnumerable data = _entity.GetType().GetSingleProperty(pro.Name).GetValue(_entity) as IEnumerable; + Utils.CheckDifference(data, list, out toremove, out toadd); + //设定子表应该更新的字段 + List setnames = new List(); + foreach (var field in FC.Keys) + { + var f = field.ToLower(); + if (f.StartsWith("entity." + pro.Name.ToLower() + "[0].")) + { + string name = f.Replace("entity." + pro.Name.ToLower() + "[0].", ""); + setnames.Add(name); + } + } + + //前台传过来的数据 + foreach (var newitem in list) + { + //数据库中的数据 + foreach (var item in data) + { + //需要更新的数据 + if (newitem.GetID().ToString() == item.GetID().ToString()) + { + dynamic i = newitem; + var newitemType = item.GetType(); + foreach (var itempro in itemPros) + { + if (!itempro.PropertyType.IsSubclassOf(typeof(TopBasePoco)) && (updateAllFields == true || setnames.Contains(itempro.Name.ToLower()))) + { + var notmapped = itempro.GetCustomAttribute(); + if (itempro.Name != "ID" && notmapped == null && itempro.PropertyType.IsList() == false) + { + DC.UpdateProperty(i, itempro.Name); + } + } + } + if (typeof(IBasePoco).IsAssignableFrom(item.GetType())) + { + DC.UpdateProperty(i, "UpdateTime"); + DC.UpdateProperty(i, "UpdateBy"); + } + } + } + } + //需要删除的数据 + foreach (var item in toremove) + { + //如果是PersistPoco,则把IsValid设为false,并不进行物理删除 + if (typeof(IPersistPoco).IsAssignableFrom(ftype)) + { + (item as IPersistPoco).IsValid = false; + if (typeof(IBasePoco).IsAssignableFrom(ftype)) + { + (item as IBasePoco).UpdateTime = DateTime.Now; + (item as IBasePoco).UpdateBy = LoginUserInfo?.ITCode; + } + dynamic i = item; + DC.UpdateEntity(i); + } + else + { + foreach (var itempro in itemPros) + { + if (itempro.PropertyType.IsSubclassOf(typeof(TopBasePoco))) + { + itempro.SetValue(item, null); + } + } + dynamic i = item; + DC.DeleteEntity(i); + } + } + //需要添加的数据 + foreach (var item in toadd) + { + if (typeof(IBasePoco).IsAssignableFrom(item.GetType())) + { + IBasePoco ent = item as IBasePoco; + if (ent.CreateTime == null) + { + ent.CreateTime = DateTime.Now; + } + if (string.IsNullOrEmpty(ent.CreateBy)) + { + ent.CreateBy = LoginUserInfo?.ITCode; + } + } + DC.AddEntity(item); + } + } + else if ((pro.GetValue(Entity) is IEnumerable list2 && list2?.Count() == 0)) + { + var itemPros = ftype.GetAllProperties(); + var _entity = DC.Set().Include(pro.Name).AsNoTracking().CheckID(Entity.GetID()).FirstOrDefault(); + if (_entity != null) + { + IEnumerable removeData = _entity.GetType().GetSingleProperty(pro.Name).GetValue(_entity) as IEnumerable; + //如果是PersistPoco,则把IsValid设为false,并不进行物理删除 + if (removeData is IEnumerable removePersistPocoData) + { + foreach (var item in removePersistPocoData) + { + (item as IPersistPoco).IsValid = false; + if (typeof(IBasePoco).IsAssignableFrom(item.GetType())) + { + (item as IBasePoco).UpdateTime = DateTime.Now; + (item as IBasePoco).UpdateBy = LoginUserInfo?.ITCode; + } + dynamic i = item; + DC.UpdateEntity(i); + } + } + else + { + foreach (var item in removeData) + { + foreach (var itempro in itemPros) + { + if (itempro.PropertyType.IsSubclassOf(typeof(TopBasePoco))) + { + itempro.SetValue(item, null); + } + } + dynamic i = item; + DC.DeleteEntity(i); + } + } + + } + } + } + } + } + #endregion + + + if (updateAllFields == false) + { + foreach (var field in FC.Keys) + { + var f = field.ToLower(); + if (f.StartsWith("entity.") && !f.Contains("[")) + { + string name = f.Replace("entity.", ""); + try + { + DC.UpdateProperty(Entity, pros.Where(x => x.Name.ToLower() == name).Select(x => x.Name).FirstOrDefault()); + } + catch (Exception) + { + } + } + } + if (typeof(IBasePoco).IsAssignableFrom(typeof(TModel))) + { + try + { + DC.UpdateProperty(Entity, "UpdateTime"); + DC.UpdateProperty(Entity, "UpdateBy"); + } + catch (Exception) + { + } + } + } + else + { + DC.UpdateEntity(Entity); + } + } + + /// + /// 删除,进行默认的删除操作。子类如有自定义操作应重载本函数 + /// + public virtual void DoDelete() + { + //如果是PersistPoco,则把IsValid设为false,并不进行物理删除 + if (typeof(IPersistPoco).IsAssignableFrom(typeof(TModel))) + { + FC.Add("Entity.IsValid", 0); + (Entity as IPersistPoco).IsValid = false; + + var pros = typeof(TModel).GetAllProperties(); + //如果包含List,将子表IsValid也设置为false + var fas = pros.Where(x => typeof(IEnumerable).IsAssignableFrom(x.PropertyType)).ToList(); + foreach (var f in fas) + { + f.SetValue(Entity, f.PropertyType.GetConstructor(Type.EmptyTypes).Invoke(null)); + } + + DoEditPrepare(false); + DC.SaveChanges(); + } + //如果是普通的TopBasePoco,则进行物理删除 + else if (typeof(TModel).GetTypeInfo().IsSubclassOf(typeof(TopBasePoco))) + { + DoRealDelete(); + } + } + + public virtual async Task DoDeleteAsync() + { + //如果是PersistPoco,则把IsValid设为false,并不进行物理删除 + if (typeof(IPersistPoco).IsAssignableFrom(typeof(TModel))) + { + FC.Add("Entity.IsValid", 0); + (Entity as IPersistPoco).IsValid = false; + var pros = typeof(TModel).GetAllProperties(); + //如果包含List,将子表IsValid也设置为false + var fas = pros.Where(x => typeof(IEnumerable).IsAssignableFrom(x.PropertyType)).ToList(); + foreach (var f in fas) + { + f.SetValue(Entity, f.PropertyType.GetConstructor(Type.EmptyTypes).Invoke(null)); + } + fas = pros.Where(x => typeof(TopBasePoco).IsAssignableFrom(x.PropertyType)).ToList(); + foreach (var f in fas) + { + f.SetValue(Entity, null); + } + DoEditPrepare(false); + try + { + await DC.SaveChangesAsync(); + } + catch (DbUpdateException) + { + MSD.AddModelError("", CoreProgram._localizer?["Sys.DeleteFailed"]); + } + } + //如果是普通的TopBasePoco,则进行物理删除 + else if (typeof(TModel).GetTypeInfo().IsSubclassOf(typeof(TopBasePoco))) + { + DoRealDelete(); + } + } + + /// + /// 物理删除,对于普通的TopBasePoco和Delete操作相同,对于PersistPoco则进行真正的删除。子类如有自定义操作应重载本函数 + /// + public virtual void DoRealDelete() + { + try + { + List fileids = new List(); + var pros = typeof(TModel).GetAllProperties(); + + //如果包含附件,则先删除附件 + var fa = pros.Where(x => x.PropertyType == typeof(FileAttachment) || typeof(TopBasePoco).IsAssignableFrom(x.PropertyType)).ToList(); + foreach (var f in fa) + { + if (f.GetValue(Entity) is FileAttachment file) + { + fileids.Add(file.ID); + } + f.SetValue(Entity, null); + } + + var fas = pros.Where(x => typeof(IEnumerable).IsAssignableFrom(x.PropertyType)).ToList(); + foreach (var f in fas) + { + var subs = f.GetValue(Entity) as IEnumerable; + if (subs == null) + { + var fullEntity = DC.Set().AsQueryable().Include(f.Name).AsNoTracking().CheckID(Entity.ID).FirstOrDefault(); + subs = f.GetValue(fullEntity) as IEnumerable; + } + if (subs != null) + { + foreach (var sub in subs) + { + fileids.Add(sub.FileId); + } + f.SetValue(Entity, null); + } + } + if (typeof(TModel) != typeof(FileAttachment)) + { + foreach (var pro in pros) + { + if (pro.PropertyType.GetTypeInfo().IsSubclassOf(typeof(TopBasePoco))) + { + pro.SetValue(Entity, null); + } + } + } + DC.DeleteEntity(Entity); + DC.SaveChanges(); + if (Wtm.ServiceProvider != null) + { + var fp = Wtm.ServiceProvider.GetRequiredService(); + foreach (var item in fileids) + { + fp.DeleteFile(item.ToString(), DC.ReCreate()); + } + } + } + catch (Exception) + { + MSD.AddModelError("", CoreProgram._localizer?["Sys.DeleteFailed"]); + } + } + + + public virtual async Task DoRealDeleteAsync() + { + try + { + List fileids = new List(); + var pros = typeof(TModel).GetAllProperties(); + + //如果包含附件,则先删除附件 + var fa = pros.Where(x => x.PropertyType == typeof(FileAttachment) || typeof(TopBasePoco).IsAssignableFrom(x.PropertyType)).ToList(); + foreach (var f in fa) + { + if (f.GetValue(Entity) is FileAttachment file) + { + fileids.Add(file.ID); + } + f.SetValue(Entity, null); + } + + var fas = pros.Where(x => typeof(IEnumerable).IsAssignableFrom(x.PropertyType)).ToList(); + foreach (var f in fas) + { + var subs = f.GetValue(Entity) as IEnumerable; + foreach (var sub in subs) + { + fileids.Add(sub.FileId); + } + f.SetValue(Entity, null); + } + if (typeof(TModel) != typeof(FileAttachment)) + { + foreach (var pro in pros) + { + if (pro.PropertyType.GetTypeInfo().IsSubclassOf(typeof(TopBasePoco))) + { + pro.SetValue(Entity, null); + } + } + } + DC.DeleteEntity(Entity); + await DC.SaveChangesAsync(); + if (Wtm.ServiceProvider != null) + { + var fp = Wtm.ServiceProvider.GetRequiredService(); + foreach (var item in fileids) + { + fp.DeleteFile(item.ToString(), DC.ReCreate()); + } + } + } + catch (Exception) + { + MSD.AddModelError("", CoreProgram._localizer?["Sys.DeleteFailed"]); + } + } + + /// + /// 创建重复数据信息 + /// + /// 重复数据信息 + /// 重复数据信息 + protected DuplicatedInfo CreateFieldsInfo(params DuplicatedField[] FieldExps) + { + DuplicatedInfo d = new DuplicatedInfo(); + d.AddGroup(FieldExps); + return d; + } + + /// + /// 创建一个简单重复数据信息 + /// + /// 重复数据的字段 + /// 重复数据信息 + public static DuplicatedField SimpleField(Expression> FieldExp) + { + return new DuplicatedField(FieldExp); + } + + /// + /// 创建一个关联到其他表数组中数据的重复信息 + /// + /// 关联表类 + /// 指向关联表类数组的Lambda + /// 指向最终字段的Lambda + /// 重复数据信息 + public static DuplicatedField SubField(Expression>> MiddleExp, params Expression>[] FieldExps) + { + return new ComplexDuplicatedField(MiddleExp, FieldExps); + } + + /// + /// 验证数据,默认验证重复数据。子类如需要其他自定义验证,则重载这个函数 + /// + /// 验证结果 + public override void Validate() + { + if (ByPassBaseValidation == false) + { + base.Validate(); + ////如果msd是BasicMSD,则认为他是手动创建的,也就是说并没有走asp.net core默认的模型验证 + ////那么手动验证模型 + //if (Wtm?.MSD is BasicMSD) + //{ + // var valContext = new ValidationContext(this.Entity); + // List error = new List(); + // if (!Validator.TryValidateObject(Entity, valContext, error, true)) + // { + // foreach (var item in error) + // { + // string key = item.MemberNames.FirstOrDefault(); + // if (MSD.Keys.Contains(key) == false) + // { + // MSD.AddModelError($"Entity.{key}", item.ErrorMessage); + // } + // } + // } + // var list = typeof(TModel).GetAllProperties().Where(x => x.PropertyType.IsListOf()); + // foreach (var item in list) + // { + // var it = item.GetValue(Entity) as IEnumerable; + // if(it == null) + // { + // continue; + // } + // var contextset = false; + // foreach (var e in it) + // { + // if(contextset == false) + // { + // valContext = new ValidationContext(e); + // contextset = true; + // } + + // if (!Validator.TryValidateObject(e, valContext, error, true)) + // { + // foreach (var err in error) + // { + // string key = err.MemberNames.FirstOrDefault(); + // if (MSD.Keys.Contains(key) == false) + // { + // MSD.AddModelError($"Entity.{item.Name}.{key}", err.ErrorMessage); + // } + // } + // } + + // } + // } + //} + + //验证重复数据 + ValidateDuplicateData(); + } + } + + /// + /// 验证重复数据 + /// 如果存在重复的数据,则返回已存在数据的id列表 + /// 如果不存在重复数据,则返回一个空列表 + /// + protected List ValidateDuplicateData() + { + //定义一个对象列表用于存放重复数据的id + var count = new List(); + //获取设定的重复字段信息 + var checkCondition = SetDuplicatedCheck(); + if (checkCondition != null && checkCondition.Groups.Count > 0) + { + //生成基础Query + var baseExp = DC.Set().AsQueryable(); + var modelType = typeof(TModel); + ParameterExpression para = Expression.Parameter(modelType, "tm"); + //循环所有重复字段组 + foreach (var group in checkCondition.Groups) + { + var innercount = new List(); + List conditions = new List(); + //生成一个表达式,类似于 x=>x.Id != id,这是为了当修改数据时验证重复性的时候,排除当前正在修改的数据 + var idproperty = typeof(TModel).GetSingleProperty("ID"); + MemberExpression idLeft = Expression.Property(para, idproperty); + ConstantExpression idRight = Expression.Constant(Entity.GetID()); + BinaryExpression idNotEqual = Expression.NotEqual(idLeft, idRight); + conditions.Add(idNotEqual); + List props = new List(); + //在每个组中循环所有字段 + foreach (var field in group.Fields) + { + Expression exp = field.GetExpression(Entity, para); + if (exp != null) + { + conditions.Add(exp); + } + //将字段名保存,为后面生成错误信息作准备 + props.AddRange(field.GetProperties()); + } + //如果要求判断id不重复,则去掉id不相等的判断,加入id相等的判断 + if (props.Any(x => x.Name.ToLower() == "id")) + { + conditions.RemoveAt(0); + BinaryExpression idEqual = Expression.Equal(idLeft, idRight); + conditions.Insert(0, idEqual); + } + //int count = 0; + if (conditions.Count > 1) + { + //循环添加条件并生成Where语句 + //Expression conExp = conditions[0]; + Expression whereCallExpression = baseExp.Expression; + for (int i = 0; i < conditions.Count; i++) + { + whereCallExpression = Expression.Call( + typeof(Queryable), + "Where", + new Type[] { modelType }, + whereCallExpression, + Expression.Lambda>(conditions[i], new ParameterExpression[] { para })); + } + var result = baseExp.Provider.CreateQuery(whereCallExpression); + + foreach (TopBasePoco res in result) + { + var id = res.GetID(); + count.Add(id); + innercount.Add(id); + } + } + if (innercount.Count > 0) + { + //循环拼接所有字段名 + string AllName = ""; + foreach (var prop in props) + { + string name = PropertyHelper.GetPropertyDisplayName(prop); + AllName += name + ","; + } + if (AllName.EndsWith(",")) + { + AllName = AllName.Remove(AllName.Length - 1); + } + //如果只有一个字段重复,则拼接形成 xxx字段重复 这种提示 + if (props.Count == 1) + { + MSD.AddModelError(GetValidationFieldName(props[0])[0], CoreProgram._localizer?["Sys.DuplicateError", AllName]); + } + //如果多个字段重复,则拼接形成 xx,yy,zz组合字段重复 这种提示 + else if (props.Count > 1) + { + MSD.AddModelError(GetValidationFieldName(props.First())[0], CoreProgram._localizer?["Sys.DuplicateGroupError", AllName]); + } + } + } + } + return count; + } + + + /// + /// 根据属性信息获取验证字段名 + /// + /// 属性信息 + /// 验证字段名称数组,用于ValidationResult + private string[] GetValidationFieldName(PropertyInfo pi) + { + return new[] { "Entity." + pi.Name }; + } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BaseImportVM.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BaseImportVM.cs new file mode 100644 index 0000000..580e54c --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BaseImportVM.cs @@ -0,0 +1,1443 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Data; +using System.Data.SqlClient; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text.Json.Serialization; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using NPOI.HSSF.Util; +using NPOI.SS.UserModel; +using NPOI.XSSF.UserModel; +using WalkingTec.Mvvm.Core.Extensions; +using WalkingTec.Mvvm.Core.Support.FileHandlers; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// 导入接口 + /// + /// 导入模版类 + public interface IBaseImport where T : BaseTemplateVM + { + T Template { get; } + byte[] GenerateTemplate(out string displayName); + void SetParms(Dictionary parms); + TemplateErrorListVM ErrorListVM { get; set; } + } + + /// + /// 导入基类,Excel导入的类应继承本类 + /// + /// 导入模版类 + /// 导入的Model类 + public class BaseImportVM : BaseVM, IBaseImport + where T : BaseTemplateVM, new() + where P : TopBasePoco, new() + { + #region 字段、属性 + /// + /// 上传文件的Id,方便导入等操作中进行绑定,这类操作需要上传文件但不需要记录在数据库中,所以Model层中没有文件Id的字段 + /// + [Display(Name = "UploadFile")] + public string UploadFileId { get; set; } + + /// + /// 下载模板显示名称 + /// + [JsonIgnore] + public string FileDisplayName { get; set; } + + /// + /// 错误列表 + /// + [JsonIgnore] + public TemplateErrorListVM ErrorListVM { get; set; } + + /// + /// 是否验证模板类型(当其他系统模板导入到某模块时可设置为False) + /// + [JsonIgnore] + public bool ValidityTemplateType { get; set; } + + /// + /// 下载模版页面的参数 + /// + [JsonIgnore] + public Dictionary Parms { get; set; } + + protected List TemplateData; + + /// + /// 要导入的Model列表 + /// + [JsonIgnore] + public List

EntityList { get; set; } + + ///

+ /// 模版 + /// + [JsonIgnore] + public T Template { get; set; } + + /// + /// Model数据是否已被赋值 + /// + protected bool isEntityListSet = false; + + /// + /// 声明XSSF + /// + protected XSSFWorkbook xssfworkbook; + + /// + /// 唯一性验证 + /// + protected DuplicatedInfo

finalInfo; + + ///

+ /// 是否存在主子表 + /// + protected bool HasSubTable { get; set; } + + /// + /// 是否在sqlserver时使用bulk导入 + /// + public bool UseBulkSave { get; set; } + + /// + /// 是否覆盖已有数据 + /// + public bool IsOverWriteExistData { get; set; } = true; + #endregion + + #region 构造函数 + public BaseImportVM() + { + ErrorListVM = new TemplateErrorListVM(); + ValidityTemplateType = true; + Template = new T(); + } + #endregion + + #region 生成excel + /// + /// 生成模版 + /// + /// 模版文件名 + /// 生成的模版 + public virtual byte[] GenerateTemplate(out string displayName) + { + return Template.GenerateTemplate(out displayName); + } + #endregion + + #region 设置参数值 + /// + /// 设置模版参数 + /// + /// 参数 + public void SetParms(Dictionary parms) + { + Template.Parms = parms; + } + #endregion + + #region 可重写方法 + + /// + /// 设置数据唯一性验证,子类中如果需要数据唯一性验证,应重写此方法 + /// + /// 唯一性属性 + public virtual DuplicatedInfo

SetDuplicatedCheck() + { + return null; + } + + ///

+ /// 获取上传的结果值 + /// + public virtual void SetEntityList() + { + if (!isEntityListSet) + { + EntityList = new List

(); + + //初始化上传的模板数据 + SetTemplateData(); + + //如果模板中有错误,直接返回 + if (ErrorListVM.EntityList.Count > 0) + { + return; + } + + //对EntityList赋值 + SetEntityData(); + + //设置标识为初始化 + isEntityListSet = true; + } + } + + ///

+ /// 获取上传模板中填写的数据,包含了对模板正确性的验证 + /// + public virtual void SetTemplateData() + { + if (TemplateData != null && TemplateData.Count > 0) + { + return; + } + + try + { + TemplateData = new List(); + xssfworkbook = new XSSFWorkbook(); + + //【CHECK】上传附件的ID为空 + if (UploadFileId == null) + { + ErrorListVM.EntityList.Add(new ErrorMessage { Message = CoreProgram._localizer?["Sys.PleaseUploadTemplate"] }); + return; + } + Models.IWtmFile file = null; + if (Wtm.ServiceProvider != null) + { + var fp = Wtm.ServiceProvider.GetRequiredService(); + file = fp.GetFile(UploadFileId, true, DC); + } + if (file == null) + { + ErrorListVM.EntityList.Add(new ErrorMessage { Message = CoreProgram._localizer?["Sys.WrongTemplate"] }); + return; + } + xssfworkbook = new XSSFWorkbook(file.DataStream); + file.DataStream.Dispose(); + Template.InitExcelData(); + Template.InitCustomFormat(); + + //【CHECK】判断是否上传的是正确的模板数据 + string TemplateHiddenName = xssfworkbook.GetSheetAt(1).GetRow(0).Cells[2].ToString(); + if (ValidityTemplateType && !TemplateHiddenName.Equals(typeof(T).Name)) + { + ErrorListVM.EntityList.Add(new ErrorMessage { Message = CoreProgram._localizer?["Sys.WrongTemplate"] }); + return; + } + + //获取数据的Sheet页信息 + ISheet sheet = xssfworkbook.GetSheetAt(0); + sheet.ForceFormulaRecalculation = true; + XSSFFormulaEvaluator XE = new XSSFFormulaEvaluator(xssfworkbook); + IEnumerator rows = sheet.GetRowEnumerator(); + var cells = sheet.GetRow(0).Cells; + + //获取模板中所有字段的属性 + List ListTemplateProptetys = new List(); + var ListPropetys = Template.GetType().GetFields().Where(x => x.FieldType == typeof(ExcelPropety)).ToList(); + for (int i = 0; i < ListPropetys.Count(); i++) + { + ExcelPropety ep = (ExcelPropety)ListPropetys[i].GetValue(Template); + ListTemplateProptetys.Add(ep); + } + + //【CHECK】验证模板的列数是否正确 + var dynamicColumn = ListTemplateProptetys.Where(x => x.DataType == ColumnDataType.Dynamic).FirstOrDefault(); + int columnCount = dynamicColumn == null ? ListTemplateProptetys.Count : (ListTemplateProptetys.Count + dynamicColumn.DynamicColumns.Count - 1); + if (columnCount != cells.Count) + { + ErrorListVM.EntityList.Add(new ErrorMessage { Message = CoreProgram._localizer?["Sys.WrongTemplate"] }); + return; + } + + //【CHECK】判断字段是否根据顺序能一对一相对应。 //是否可以去除? + int pIndex = 0; + HasSubTable = false; + for (int i = 0; i < cells.Count; i++) + { + //是否有子表 + HasSubTable = ListTemplateProptetys[pIndex].SubTableType != null ? true : HasSubTable; + //if (ListTemplateProptetys[pIndex].DataType != ColumnDataType.Dynamic) + //{ + // if (cells[i].ToString().Trim('*') != ListTemplateProptetys[pIndex].ColumnName) + // { + // ErrorListVM.EntityList.Add(new ErrorMessage { Message = CoreProgram._localizer?["Sys.WrongTemplate"] }); + // return; + // } + // pIndex++; + //} + //else + //{ + // var listDynamicColumns = ListTemplateProptetys[i].DynamicColumns; + // int dcCount = listDynamicColumns.Count; + // for (int dclIndex = 0; dclIndex < dcCount; dclIndex++) + // { + // if (cells[i].ToString().Trim('*') != listDynamicColumns[dclIndex].ColumnName) + // { + // ErrorListVM.EntityList.Add(new ErrorMessage { Message = CoreProgram._localizer?["Sys.WrongTemplate"] }); + // break; + // } + // i = i + 1; + // } + // i = i - 1; + pIndex++; + //} + } + + //如果有子表,则设置主表字段非必填 + if (HasSubTable) + { + for (int i = 0; i < cells.Count; i++) + { + ListTemplateProptetys[i].IsNullAble = ListTemplateProptetys[i].SubTableType == null ? true : ListTemplateProptetys[i].IsNullAble; + } + } + + //向TemplateData中赋值 + int rowIndex = 2; + rows.MoveNext(); + while (rows.MoveNext()) + { + XSSFRow row = (XSSFRow)rows.Current; + if (IsEmptyRow(row, columnCount)) + { + return; + } + + T result = new T(); + pIndex = 0; + for (int i = 0; i < columnCount; i++) + { + //获取列的值 + string value = row.GetCell(i, MissingCellPolicy.CREATE_NULL_AS_BLANK).ToString(); + ExcelPropety excelPropety = CopyExcelPropety(ListTemplateProptetys[pIndex]); + + if (excelPropety.DataType == ColumnDataType.Text) + { + ICell cell = row.GetCell(i); + value = GetCellFormulaValue(XE, cell, value); + } + + if (excelPropety.DataType == ColumnDataType.Dynamic) + { + int dynamicColCount = excelPropety.DynamicColumns.Count(); + for (int dynamicColIndex = 0; dynamicColIndex < dynamicColCount; dynamicColIndex++) + { + excelPropety.DynamicColumns[dynamicColIndex].ValueValidity(row.GetCell(i + dynamicColIndex, MissingCellPolicy.CREATE_NULL_AS_BLANK).ToString(), ErrorListVM.EntityList, rowIndex); + } + i = i + dynamicColCount - 1; + } + else + { + excelPropety.ValueValidity(value, ErrorListVM.EntityList, rowIndex); + } + + //如果没有错误,进行赋值 + if (ErrorListVM.EntityList.Count == 0) + { + var pts = ListPropetys[pIndex]; + pts.SetValue(result, excelPropety); + } + + pIndex++; + } + result.ExcelIndex = rowIndex; + TemplateData.Add(result); + rowIndex++; + } + + return; + } + catch + { + ErrorListVM.EntityList.Add(new ErrorMessage { Message = CoreProgram._localizer?["Sys.WrongTemplate"] }); + } + } + + #region 进行公式计算 + public string GetCellFormulaValue(XSSFFormulaEvaluator XE, ICell cell, string Value) + { + if (!string.IsNullOrEmpty(Value) && Value.IndexOf("=") == 0) + { + try + { + string Formula = Value.Substring(1); + cell.SetCellFormula(Formula); + XE.EvaluateFormulaCell(cell); + Value = cell.NumericCellValue.ToString(); + } + catch (Exception) + { + } + } + return Value; + } + #endregion + + /// + /// 根据模板中的数据,填写导入类的集合中 + /// + public virtual void SetEntityData() + { + //反射出类中所有属性字段 P是Model层定义的类 + var pros = typeof(P).GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); + + //反射出模板类中的所有属性字段 T是模板类,ExcelProperty 是自定义的Excel属性类 + List ListExcelFields = typeof(T).GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance).Where(x => x.FieldType == typeof(ExcelPropety)).ToList(); + + //循环Excel中的数据 + foreach (var item in TemplateData) + { + int rowIndex = 2; + bool isMainData = false; + + //主表信息 + Dictionary ParentEntity = new Dictionary(); + string ParentEntityValues = string.Empty; + + //子表信息 + Dictionary> ChildrenEntity = new Dictionary>(); + Dictionary ChildrenEntityDic = new Dictionary(); + + //循环TemplateVM中定义的所有的列,区分出主子表 + foreach (var ExcelField in ListExcelFields) + { + //获取本列的ExcelProperty的值 + if (typeof(T).GetField(ExcelField.Name).GetValue(item) is ExcelPropety ep) + { + //如果是子表的字段 + if (ep.SubTableType != null) + { + //保存子表字段信息稍后处理 + if (!ChildrenEntity.ContainsKey(ep.SubTableType)) + { + ChildrenEntity[ep.SubTableType] = new List(); + } + ChildrenEntity[ep.SubTableType].Add(ExcelField); + } + else + { + //保存子表字段信息稍后处理 + ParentEntity.Add(ep.FieldName, ep); + ParentEntityValues += ep.Value; + } + } + } + + //子表信息是否为空 + foreach (var sub in ChildrenEntity) + { + string subVal = string.Empty; + foreach (var field in sub.Value) + { + ExcelPropety ep = typeof(T).GetField(field.Name).GetValue(item) as ExcelPropety; + subVal += ep.Value; + } + ChildrenEntityDic.Add(sub.Key, subVal); + } + + P entity = null; + + //说明主表信息为空 + if (string.IsNullOrEmpty(ParentEntityValues)) + { + entity = EntityList.LastOrDefault(); + } + else + { + //初始化一个新的Entity + entity = new P(); + isMainData = true; + + //给主表赋值 + foreach (var mep in ParentEntity) + { + SetEntityFieldValue(entity, mep.Value, rowIndex, mep.Key, item); + } + } + + //给子表赋值 + foreach (var sub in ChildrenEntity) + { + //循环Entity的所有属性,找到List类型的字段 + foreach (var pro in pros) + { + if (pro.PropertyType.IsGenericType) + { + var gtype = pro.PropertyType.GetGenericArguments()[0]; + if (gtype == sub.Key) + { + //子表 + var subList = entity.GetType().GetSingleProperty(pro.Name).GetValue(entity); + string fk = DC.GetFKName

(pro.Name); + + //如果子表不为空 + if (!string.IsNullOrEmpty(ChildrenEntityDic.Where(x => x.Key == sub.Key).FirstOrDefault().Value)) + { + IList list = null; + if (subList == null) + { + //初始化List + list = typeof(List<>).MakeGenericType(gtype).GetConstructor(Type.EmptyTypes).Invoke(null) as IList; + } + else + { + list = subList as IList; + } + + //初始化一个SubTableType + var SubTypeEntity = gtype.GetConstructor(System.Type.EmptyTypes).Invoke(null); + + //给SubTableType中和本ExcelProperty同名的字段赋值 + foreach (var field in sub.Value) + { + ExcelPropety ep = typeof(T).GetField(field.Name).GetValue(item) as ExcelPropety; + SetEntityFieldValue(SubTypeEntity, ep, rowIndex, ep.FieldName, item); + } + + if (string.IsNullOrEmpty(fk) == false) + { + PropertyHelper.SetPropertyValue(SubTypeEntity, fk, entity.GetID()); + } + + if (typeof(IBasePoco).IsAssignableFrom(SubTypeEntity.GetType())) + { + (SubTypeEntity as IBasePoco).CreateTime = DateTime.Now; + (SubTypeEntity as IBasePoco).CreateBy = LoginUserInfo?.ITCode; + } + //var context = new ValidationContext(SubTypeEntity); + //var validationResults = new List(); + //TryValidateObject(SubTypeEntity, context, validationResults); + //if (validationResults.Count == 0) + //{ + //将付好值得SubTableType实例添加到List中 + list.Add(SubTypeEntity); + + PropertyHelper.SetPropertyValue(entity, pro.Name, list); + //} + //else + //{ + // ErrorListVM.EntityList.Add(new ErrorMessage { Message = validationResults.FirstOrDefault()?.ErrorMessage ?? "Error", ExcelIndex = item.ExcelIndex }); + // break; + //} + + } + break; + } + } + } + + } + entity.ExcelIndex = item.ExcelIndex; + if (isMainData) + { + EntityList.Add(entity); + } + } + } + + ///

+ /// 进行上传中的错误验证 + /// + public virtual void SetValidateCheck() + { + //找到对应的BaseCRUDVM,并初始化 + var vms = this.GetType().Assembly.GetExportedTypes().Where(x => x.IsSubclassOf(typeof(BaseCRUDVM

))).ToList(); + var vmtype = vms.Where(x => x.Name.ToLower() == typeof(P).Name.ToLower() + "vm").FirstOrDefault(); + if (vmtype == null) + { + vmtype = vms.FirstOrDefault(); + } + + IBaseCRUDVM

vm = null; + DuplicatedInfo

dinfo = null; + if (vmtype != null) + { + vm = vmtype.GetConstructor(System.Type.EmptyTypes).Invoke(null) as IBaseCRUDVM

; + vm.CopyContext(this); + dinfo = (vm as dynamic).SetDuplicatedCheck(); + } + var cinfo = this.SetDuplicatedCheck(); + finalInfo = new DuplicatedInfo

+ { + Groups = new List>() + }; + if (cinfo != null) + { + foreach (var item in cinfo?.Groups) + { + finalInfo.Groups.Add(item); + } + } + else if (dinfo != null) + { + foreach (var item in dinfo?.Groups) + { + finalInfo.Groups.Add(item); + } + } + //调用controller方法验证model + //var vmethod = Controller?.GetType().GetMethod("RedoValidation"); + foreach (var entity in EntityList) + { + //try + //{ + // vmethod.Invoke(Controller, new object[] { entity }); + //} + //catch { } + + if (vm != null) + { + vm.SetEntity(entity); + vm.ByPassBaseValidation = true; + vm.Validate(); + var basevm = vm as BaseVM; + if (basevm?.MSD?.Count > 0) + { + foreach (var key in basevm.MSD.Keys) + { + foreach (var error in basevm.MSD[key]) + { + ErrorListVM.EntityList.Add(new ErrorMessage { Message = error.ErrorMessage, Index = entity.ExcelIndex }); + } + } + } + } + (vm as BaseVM)?.MSD.Clear(); + + //在本地EntityList中验证是否有重复 + ValidateDuplicateData(finalInfo, entity); + } + } + + protected void SetEntityFieldValue(object entity, ExcelPropety ep, int rowIndex, string fieldName, T templateVM) + { + if (ep.FormatData != null) + { + ProcessResult processResult = ep.FormatData(ep.Value, templateVM); + if (processResult != null) + { + //未添加任何处理结果 + if (processResult.EntityValues.Count == 0) + { + PropertyHelper.SetPropertyValue(entity, fieldName, ep.Value, stringBasedValue: true); + } + //字段为一对一 + if (processResult.EntityValues.Count == 1) + { + ep.Value = processResult.EntityValues[0].FieldValue; + if (!string.IsNullOrEmpty(processResult.EntityValues[0].ErrorMsg)) + { + ErrorListVM.EntityList.Add(new ErrorMessage { Message = processResult.EntityValues[0].ErrorMsg, ExcelIndex = rowIndex, Index = rowIndex }); + } + PropertyHelper.SetPropertyValue(entity, fieldName, ep.Value, stringBasedValue: true); + } + //字段为一对多 + if (processResult.EntityValues.Count > 1) + { + foreach (var entityValue in processResult.EntityValues) + { + if (!string.IsNullOrEmpty(entityValue.ErrorMsg)) + { + ErrorListVM.EntityList.Add(new ErrorMessage { Message = entityValue.ErrorMsg, ExcelIndex = rowIndex, Index = rowIndex }); + } + PropertyHelper.SetPropertyValue(entity, entityValue.FieldName, entityValue.FieldValue, stringBasedValue: true); + } + } + } + } + else if (ep.FormatSingleData != null) + { + ep.FormatSingleData(ep.Value, templateVM, out string singleEntityValue, out string errorMsg); + if (!string.IsNullOrEmpty(errorMsg)) + { + ErrorListVM.EntityList.Add(new ErrorMessage { Message = errorMsg, ExcelIndex = rowIndex, Index = rowIndex }); + } + PropertyHelper.SetPropertyValue(entity, fieldName, singleEntityValue, stringBasedValue: true); + } + else + { + PropertyHelper.SetPropertyValue(entity, fieldName, ep.Value, stringBasedValue: true); + } + } + + protected bool IsUpdateRecordDuplicated(DuplicatedInfo

checkCondition, P entity) + { + if (checkCondition != null && checkCondition.Groups.Count > 0) + { + //生成基础Query + var baseExp = EntityList.AsQueryable(); + var modelType = typeof(P); + ParameterExpression para = Expression.Parameter(modelType, "tm"); + //循环所有重复字段组 + foreach (var group in checkCondition.Groups) + { + List conditions = new List(); + //生成一个表达式,类似于 x=>x.Id != id,这是为了当修改数据时验证重复性的时候,排除当前正在修改的数据 + var idproperty = modelType.GetSingleProperty("ID"); + MemberExpression idLeft = Expression.Property(para, idproperty); + ConstantExpression idRight = Expression.Constant(entity.GetID()); + BinaryExpression idNotEqual = Expression.NotEqual(idLeft, idRight); + conditions.Add(idNotEqual); + List props = new List(); + //在每个组中循环所有字段 + foreach (var field in group.Fields) + { + Expression exp = field.GetExpression(entity, para); + if (exp != null) + { + conditions.Add(exp); + } + //将字段名保存,为后面生成错误信息作准备 + props.AddRange(field.GetProperties()); + } + int count = 0; + if (conditions.Count > 1) + { + //循环添加条件并生成Where语句 + Expression conExp = conditions[0]; + for (int i = 1; i < conditions.Count; i++) + { + conExp = Expression.And(conExp, conditions[i]); + } + + MethodCallExpression whereCallExpression = Expression.Call( + typeof(Queryable), + "Where", + new Type[] { modelType }, + baseExp.Expression, + Expression.Lambda>(conExp, new ParameterExpression[] { para })); + var result = baseExp.Provider.CreateQuery(whereCallExpression); + + foreach (var res in result) + { + count++; + } + } + if (count > 0) + { + return true; + } + } + } + return false; + } + + protected void ValidateDuplicateData(DuplicatedInfo

checkCondition, P entity) + { + if (checkCondition != null && checkCondition.Groups.Count > 0) + { + //生成基础Query + var baseExp = EntityList.AsQueryable(); + var modelType = typeof(P); + ParameterExpression para = Expression.Parameter(modelType, "tm"); + //循环所有重复字段组 + foreach (var group in checkCondition.Groups) + { + List conditions = new List(); + //生成一个表达式,类似于 x=>x.Id != id,这是为了当修改数据时验证重复性的时候,排除当前正在修改的数据 + var idproperty = modelType.GetSingleProperty("ExcelIndex"); + MemberExpression idLeft = Expression.Property(para, idproperty); + ConstantExpression idRight = Expression.Constant(entity.ExcelIndex); + BinaryExpression idNotEqual = Expression.NotEqual(idLeft, idRight); + conditions.Add(idNotEqual); + List props = new List(); + //在每个组中循环所有字段 + foreach (var field in group.Fields) + { + Expression exp = field.GetExpression(entity, para); + if (exp != null) + { + conditions.Add(exp); + } + //将字段名保存,为后面生成错误信息作准备 + props.AddRange(field.GetProperties()); + } + int count = 0; + if (conditions.Count > 1) + { + //循环添加条件并生成Where语句 + Expression whereCallExpression = baseExp.Expression; + for (int i = 0; i < conditions.Count; i++) + { + whereCallExpression = Expression.Call( + typeof(Queryable), + "Where", + new Type[] { modelType }, + whereCallExpression, + Expression.Lambda>(conditions[i], new ParameterExpression[] { para })); + } + var result = baseExp.Provider.CreateQuery(whereCallExpression); + + foreach (var res in result) + { + count++; + } + } + if (count > 0) + { + //循环拼接所有字段名 + string AllName = ""; + foreach (var prop in props) + { + string name = PropertyHelper.GetPropertyDisplayName(prop); + AllName += name + ","; + } + if (AllName.EndsWith(",")) + { + AllName = AllName.Remove(AllName.Length - 1); + } + //如果只有一个字段重复,则拼接形成 xxx字段重复 这种提示 + if (props.Count == 1) + { + ErrorListVM.EntityList.Add(new ErrorMessage { Message = CoreProgram._localizer?["Sys.DuplicateError", AllName], Index = entity.ExcelIndex }); + } + //如果多个字段重复,则拼接形成 xx,yy,zz组合字段重复 这种提示 + else if (props.Count > 1) + { + ErrorListVM.EntityList.Add(new ErrorMessage { Message = CoreProgram._localizer?["Sys.DuplicateGroupError", AllName], Index = entity.ExcelIndex }); + } + } + } + } + } + + + private void TryValidateObject(object model, ValidationContext context, ICollection results) + { + var modelType = model.GetType(); + foreach (var p in modelType.GetProperties()) + { + var propertyValue = p.GetValue(model); + TryValidateProperty(propertyValue, context, results, p); + } + } + + private void TryValidateProperty(object value, ValidationContext context, ICollection results, PropertyInfo propertyInfo = null) + { + var modelType = context.ObjectType; + if (propertyInfo == null) + { + propertyInfo = modelType.GetProperty(context.MemberName!); + } + + if (propertyInfo != null) + { + var rules = propertyInfo.GetCustomAttributes(true).Where(i => i.GetType().BaseType == typeof(ValidationAttribute)).Cast(); + var displayName = propertyInfo.GetPropertyDisplayName(); + var memberName = propertyInfo.Name; + foreach (var rule in rules) + { + if (!rule.IsValid(value)) + { + string errorMessage = "Error"; + if (!string.IsNullOrEmpty(rule.ErrorMessage)) + { + if (rule is RangeAttribute range) + { + if (range.Minimum != null && range.Maximum != null) + { + errorMessage = Wtm.Localizer[rule.ErrorMessage, displayName, range.Minimum, range.Maximum]; + } + else if (range.Minimum != null) + { + errorMessage = Wtm.Localizer[rule.ErrorMessage, displayName, range.Minimum]; + } + else if (range.Maximum != null) + { + errorMessage = Wtm.Localizer[rule.ErrorMessage, displayName, range.Maximum]; + } + } + else if (rule is StringLengthAttribute sl) + { + if (sl.MaximumLength > 0 && sl.MinimumLength > 0) + { + errorMessage = Wtm.Localizer[rule.ErrorMessage, displayName, sl.MinimumLength, sl.MaximumLength]; + } + else if (sl.MinimumLength > 0) + { + errorMessage = Wtm.Localizer[rule.ErrorMessage, displayName, sl.MinimumLength]; + } + else if (sl.MaximumLength > 0) + { + errorMessage = Wtm.Localizer[rule.ErrorMessage, displayName, sl.MaximumLength]; + } + } + else + { + errorMessage = Wtm.Localizer[rule.ErrorMessage, displayName]; + } + } + results.Add(new ValidationResult(errorMessage, new string[] { memberName })); + } + } + } + } + + + ///

+ /// 保存指定表中的数据 + /// + /// 成功返回True,失败返回False + public virtual bool BatchSaveData() + { + //删除不必要的附件 + if (DeletedFileIds != null && DeletedFileIds.Count > 0 && Wtm.ServiceProvider != null) + { + var fp = Wtm.ServiceProvider.GetRequiredService(); + + foreach (var item in DeletedFileIds) + { + fp.DeleteFile(item.ToString(), DC.ReCreate()); + } + } + + //进行赋值 + SetEntityList(); + foreach (var entity in EntityList) + { + var context = new ValidationContext(entity); + var validationResults = new List(); + TryValidateObject(entity, context, validationResults); + if (validationResults.Count > 0) + { + ErrorListVM.EntityList.Add(new ErrorMessage { Message = validationResults.FirstOrDefault()?.ErrorMessage ?? "Error", ExcelIndex = entity.ExcelIndex, Index = entity.ExcelIndex }); + } + } + if (ErrorListVM.EntityList.Count > 0) + { + DoReInit(); + return false; + } + + //执行验证 + SetValidateCheck(); + if (ErrorListVM.EntityList.Count > 0) + { + DoReInit(); + return false; + } + + //循环数据列表 + List

ListAdd = new List

(); + foreach (var item in EntityList) + { + //根据唯一性的设定查找数据库中是否有同样的数据 + P exist = IsDuplicateData(item, finalInfo); + //如果设置了覆盖功能 + if (IsOverWriteExistData) + { + if (exist != null) + { + //如果有重复数据,则进行修改 + var tempPros = typeof(T).GetFields(); + foreach (var pro in tempPros) + { + var excelProp = Template.GetType().GetField(pro.Name).GetValue(Template) as ExcelPropety; + var proToSet = typeof(P).GetSingleProperty(excelProp.FieldName); + if (proToSet != null) + { + var val = proToSet.GetValue(item); + PropertyHelper.SetPropertyValue(exist, excelProp.FieldName, val, stringBasedValue: true); + try + { + DC.UpdateProperty(exist, proToSet.Name); + } + catch { } + } + } + + if (tempPros.Where(x => x.Name == "UpdateTime").SingleOrDefault() == null) + { + if (typeof(IBasePoco).IsAssignableFrom(exist.GetType())) + { + (exist as IBasePoco).UpdateTime = DateTime.Now; + DC.UpdateProperty(exist, "UpdateTime"); + } + } + + if (tempPros.Where(x => x.Name == "UpdateBy").SingleOrDefault() == null) + { + if (typeof(IBasePoco).IsAssignableFrom(exist.GetType())) + { + (exist as IBasePoco).UpdateBy = LoginUserInfo.ITCode; + DC.UpdateProperty(exist, "UpdateBy"); + } + } + exist.ExcelIndex = item.ExcelIndex; + //DC.UpdateEntity(exist); + + continue; + } + else + { + if (typeof(IPersistPoco).IsAssignableFrom(item.GetType())) + { + (item as IPersistPoco).IsValid = true; + } + } + } + else + { + if (exist == null) + { + if (typeof(IPersistPoco).IsAssignableFrom(item.GetType())) + { + (item as IPersistPoco).IsValid = true; + } + } + } + //进行添加操作 + if (typeof(IBasePoco).IsAssignableFrom(item.GetType())) + { + (item as IBasePoco).CreateTime = DateTime.Now; + (item as IBasePoco).CreateBy = LoginUserInfo?.ITCode; + } + //如果是SqlServer数据库,而且没有主子表功能,进行Bulk插入 + if (ConfigInfo.Connections.Where(x => x.Key == (CurrentCS ?? "default")).FirstOrDefault().DbType == DBTypeEnum.SqlServer && !HasSubTable && UseBulkSave == true) + { + ListAdd.Add(item); + } + else + { + DC.Set

().Add(item); + } + } + + if (ErrorListVM.EntityList.Count > 0) + { + DoReInit(); + return false; + } + + //如果没有错误,更新数据库 + if (EntityList.Count > 0) + { + try + { + DC.SaveChanges(); + + if (ListAdd.Count > 0) + { + BulkInsert

(DC, DC.GetTableName

(), ListAdd); + } + } + catch (Exception e) + { + SetExceptionMessage(e, null); + DoReInit(); + return false; + } + } + if (string.IsNullOrEmpty(UploadFileId) == false && Wtm.ServiceProvider != null) + { + var fp = Wtm.ServiceProvider.GetRequiredService(); + fp.DeleteFile(UploadFileId, DC.ReCreate()); + } + + return true; + } + + ///

+ /// 批量插入数据库操作,支持SqlServer + /// + /// + /// data context + /// + /// + protected static void BulkInsert(IDataContext dc, string tableName, IList list) + { + using (var bulkCopy = new SqlBulkCopy(dc.CSName)) + { + bulkCopy.BatchSize = list.Count; + bulkCopy.DestinationTableName = tableName; + + var table = new DataTable(); + var props = typeof(K).GetAllProperties().Distinct(x => x.Name); + + //生成Table的列 + foreach (var propertyInfo in props) + { + var notmapped = propertyInfo.GetCustomAttribute(); + var notobject = propertyInfo.PropertyType.Namespace.Equals("System") || propertyInfo.PropertyType.IsEnumOrNullableEnum(); + if (notmapped == null && notobject) + { + string Name = dc.GetFieldName(propertyInfo.Name); + bulkCopy.ColumnMappings.Add(Name, Name); + table.Columns.Add(Name, Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType); + } + } + + //给Table赋值 + var values = new object[table.Columns.Count]; + foreach (var item in list) + { + var Index = 0; + foreach (var propertyInfo in props) + { + var notmapped = propertyInfo.GetCustomAttribute(); + var notobject = propertyInfo.PropertyType.Namespace.Equals("System") || propertyInfo.PropertyType.IsEnumOrNullableEnum(); + if (notmapped == null && notobject) + { + values[Index] = propertyInfo.GetValue(item); + Index++; + } + } + table.Rows.Add(values); + } + //检测是否有继承字段,如果存在,进行赋值 + string Discriminator = dc.GetFieldName("Discriminator"); + if (!string.IsNullOrEmpty(Discriminator)) + { + bulkCopy.ColumnMappings.Add("Discriminator", "Discriminator"); + table.Columns.Add("Discriminator", typeof(string)); + for (int i = 0; i < table.Rows.Count; i++) + { + table.Rows[i]["Discriminator"] = typeof(K).Name; + } + } + bulkCopy.WriteToServer(table); + } + } + #endregion + + #region 验证是否空行 + /// + /// 验证Excel中某行是否为空行 + /// + /// 行数 + /// 列数 + /// True代表空行,False代表非空行 + private bool IsEmptyRow(XSSFRow row, int colCount) + { + bool result = true; + for (int i = 0; i < colCount; i++) + { + string value = row.GetCell(i, MissingCellPolicy.CREATE_NULL_AS_BLANK).ToString(); + if (!string.IsNullOrEmpty(value)) + { + result = false; + break; + } + } + return result; + } + #endregion + + #region 复制Excel属性 + /// + /// 复制Excel属性 + /// + /// 单元格属性 + /// 复制后的单元格 + private ExcelPropety CopyExcelPropety(ExcelPropety excelPropety) + { + ExcelPropety ep = new ExcelPropety + { + BackgroudColor = excelPropety.BackgroudColor, + ColumnName = excelPropety.ColumnName, + DataType = excelPropety.DataType, + ResourceType = excelPropety.ResourceType, + IsNullAble = excelPropety.IsNullAble, + ListItems = excelPropety.ListItems, + MaxValuseOrLength = excelPropety.MaxValuseOrLength, + MinValueOrLength = excelPropety.MinValueOrLength, + Value = excelPropety.Value, + SubTableType = excelPropety.SubTableType, + CharCount = excelPropety.CharCount, + ReadOnly = excelPropety.ReadOnly, + FormatData = excelPropety.FormatData, + FormatSingleData = excelPropety.FormatSingleData, + FieldName = excelPropety.FieldName + }; + List li = new List(); + foreach (var item in excelPropety.DynamicColumns) + { + li.Add(CopyExcelPropety(item)); + } + ep.DynamicColumns = li; + return ep; + } + #endregion + + #region 设置异常信息 + /// + /// 设置错误信息 + /// + /// 异常 + /// 数据Id + protected void SetExceptionMessage(Exception e, long? id) + { + //检查是否为数据库操作错误 + if (e is DbUpdateException) + { + var de = e as DbUpdateException; + if (de.Entries != null) + { + if (de.Entries.Count == 0) + { + ErrorListVM.EntityList.Add(new ErrorMessage { Index = 0, Message = e.Message + e.InnerException?.Message }); + } + //循环此错误相关的数据 + foreach (var ent in de.Entries) + { + //获取错误数据Id + var errorId = (long)((ent.Entity as TopBasePoco).ExcelIndex); + //根据State判断修改或删除操作,输出不同的错误信息 + if (ent.State == EntityState.Deleted) + { + ErrorListVM.EntityList.Add(new ErrorMessage { Index = errorId, Message = CoreProgram._localizer?["Sys.DataCannotDelete"] }); + } + else if (ent.State == EntityState.Modified) + { + ErrorListVM.EntityList.Add(new ErrorMessage { Index = errorId, Message = CoreProgram._localizer?["Sys.EditFailed"] }); + } + else + { + ErrorListVM.EntityList.Add(new ErrorMessage { Index = errorId, Message = de.Message }); + } + } + } + } + //对于其他类型的错误,直接添加错误信息 + else + { + if (id != null) + { + ErrorListVM.EntityList.Add(new ErrorMessage { Index = id.Value, Message = e.Message }); + } + else + { + ErrorListVM.EntityList.Add(new ErrorMessage { Index = 0, Message = e.Message }); + } + } + } + #endregion + + #region 验证数据重复 + + /// + /// 判断数据是否在库中存在重复数据 + /// + /// 要验证的数据 + /// 验证表达式 + /// null代表没有重复 + protected P IsDuplicateData(P Entity, DuplicatedInfo

checkCondition) + { + //获取设定的重复字段信息 + if (checkCondition != null && checkCondition.Groups.Count > 0) + { + //生成基础Query + var baseExp = DC.Set

().AsQueryable(); + var modelType = typeof(P); + ParameterExpression para = Expression.Parameter(modelType, "tm"); + //循环所有重复字段组 + foreach (var group in checkCondition.Groups) + { + List conditions = new List(); + //生成一个表达式,类似于 x=>x.Id != id,这是为了当修改数据时验证重复性的时候,排除当前正在修改的数据 + //在每个组中循环所有字段 + List props = new List(); + //在每个组中循环所有字段 + foreach (var field in group.Fields) + { + Expression exp = field.GetExpression(Entity, para); + if (exp != null) + { + conditions.Add(exp); + } + //将字段名保存,为后面生成错误信息作准备 + props.AddRange(field.GetProperties()); + } + + if (conditions.Count > 0) + { + //循环添加条件并生成Where语句 + Expression whereCallExpression = baseExp.Expression; + for (int i = 0; i < conditions.Count; i++) + { + whereCallExpression = Expression.Call( + typeof(Queryable), + "Where", + new Type[] { modelType }, + whereCallExpression, + Expression.Lambda>(conditions[i], new ParameterExpression[] { para })); + } + var result = baseExp.Provider.CreateQuery(whereCallExpression); + + + foreach (var res in result) + { + if (IsOverWriteExistData == false) + { + //循环拼接所有字段名 + string AllName = ""; + foreach (var prop in props) + { + string name = PropertyHelper.GetPropertyDisplayName(prop); + AllName += name + ","; + } + if (AllName.EndsWith(",")) + { + AllName = AllName.Remove(AllName.Length - 1); + } + //如果只有一个字段重复,则拼接形成 xxx字段重复 这种提示 + if (props.Count == 1) + { + ErrorListVM.EntityList.Add(new ErrorMessage { Message = CoreProgram._localizer?["Sys.DuplicateError", AllName], Index = Entity.ExcelIndex }); + } + //如果多个字段重复,则拼接形成 xx,yy,zz组合字段重复 这种提示 + else if (props.Count > 1) + { + ErrorListVM.EntityList.Add(new ErrorMessage { Message = CoreProgram._localizer?["Sys.DuplicateGroupError", AllName], Index = Entity.ExcelIndex }); + } + + } + return res as P; + } + } + } + } + return null; + } + #endregion + + protected DuplicatedInfo

CreateFieldsInfo(params DuplicatedField

[] FieldExps) + { + DuplicatedInfo

d = new DuplicatedInfo

(); + d.AddGroup(FieldExps); + return d; + } + + ///

+ /// 创建一个简单重复数据信息 + /// + /// 重复数据的字段 + /// 重复数据信息 + public static DuplicatedField

SimpleField(Expression> FieldExp) + { + return new DuplicatedField

(FieldExp); + } + + ///

+ /// 创建一个关联到其他表数组中数据的重复信息 + /// + /// 关联表类 + /// 指向关联表类数组的Lambda + /// 指向最终字段的Lambda + /// 重复数据信息 + public static DuplicatedField

SubField(Expression>> MiddleExp, params Expression>[] FieldExps) + { + return new ComplexDuplicatedField(MiddleExp, FieldExps); + } + + public ErrorObj GetErrorJson() + { + var mse = new ErrorObj(); + mse.Form = new Dictionary(); + var err = ErrorListVM?.EntityList?.Where(x => x.Index == 0).FirstOrDefault()?.Message; + if (string.IsNullOrEmpty(err)) + { + Models.IWtmFile fa = null; + if(Wtm.ServiceProvider == null) { + return mse; + } + var fp = Wtm.ServiceProvider.GetRequiredService(); + fa = fp.GetFile(UploadFileId, true, DC); + xssfworkbook = new XSSFWorkbook(fa.DataStream); + fa.DataStream.Dispose(); + var propetys = Template.GetType().GetFields().Where(x => x.FieldType == typeof(ExcelPropety)).ToList(); + List excelPropetys = new List(); + for (int porpetyIndex = 0; porpetyIndex < propetys.Count(); porpetyIndex++) + { + ExcelPropety ep = (ExcelPropety)propetys[porpetyIndex].GetValue(Template); + excelPropetys.Add(ep); + } + int columnCount = excelPropetys.Count; + //int excelPropetyCount = excelPropetys.Count; + var dynamicColumn = excelPropetys.Where(x => x.DataType == ColumnDataType.Dynamic).FirstOrDefault(); + if (dynamicColumn != null) + { + columnCount = columnCount + dynamicColumn.DynamicColumns.Count - 1; + } + ISheet sheet = xssfworkbook.GetSheetAt(0); + var errorStyle = xssfworkbook.CreateCellStyle(); + IFont f = xssfworkbook.CreateFont(); + f.Color = HSSFColor.Red.Index; + errorStyle.SetFont(f); + errorStyle.IsLocked = true; + foreach (var e in ErrorListVM?.EntityList) + { + if (e.Index > 0) + { + var c = sheet.GetRow((int)(e.Index - 1)).CreateCell(columnCount); + c.CellStyle = errorStyle; + c.SetCellValue(e.Message); + } + } + MemoryStream ms = new MemoryStream(); + xssfworkbook.Write(ms); + ms.Position = 0; + + var newfile = fp.Upload("Error-" + fa.FileName, ms.Length, ms); + ms.Close(); + ms.Dispose(); + err = CoreProgram._localizer?["Sys.ImportError"]; + mse.Form.Add("Entity.Import", err); + mse.Form.Add("Entity.ErrorFileId", newfile.GetID()); + } + else + { + mse.Form.Add("Entity.Import", err); + } + return mse; + } + } + + #region 辅助类 + public class ErrorMessage : TopBasePoco + { + [Display(Name = "Sys.RowIndex")] + public long Index { get; set; } + + [Display(Name = "Sys.CellIndex")] + public long Cell { get; set; } + [Display(Name = "Sys.ErrorMsg")] + public string Message { get; set; } + } + + ///

+ /// 错误数据列表 + /// + public class TemplateErrorListVM : BasePagedListVM + { + + public TemplateErrorListVM() + { + EntityList = new List(); + NeedPage = false; + } + + protected override IEnumerable> InitGridHeader() + { + return new List>{ + this.MakeGridHeader(x => x.Index, 60), + this.MakeGridHeader(x => x.Message) + }; + } + + public override IOrderedQueryable GetSearchQuery() + { + return EntityList.AsQueryable().OrderBy(x => x.Index); + } + } + + #endregion + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BasePagedListVM.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BasePagedListVM.cs new file mode 100644 index 0000000..42ef418 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BasePagedListVM.cs @@ -0,0 +1,1228 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.Data; +using System.Data.Common; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using MySqlConnector; +using Npgsql; +using NpgsqlTypes; +using NPOI.HSSF.Util; +using NPOI.SS.UserModel; +using NPOI.SS.Util; +using NPOI.XSSF.UserModel; +using WalkingTec.Mvvm.Core.Extensions; + +namespace WalkingTec.Mvvm.Core +{ + public delegate object ColumnFormatCallBack(T entity, object fieldValue) where T : TopBasePoco; + + /// + /// ListVM的搜索模式枚举 + /// + public enum ListVMSearchModeEnum + { + Search, //搜索 + Export, //导出 + Batch, //批量 + Selector,//选择器 + MasterDetail, // + CheckExport, + Custom1, Custom2, Custom3, Custom4, Custom5 + }; + + /// + /// ListVM的基类,所有ListVM应该继承这个类, 基类提供了搜索,导出等列表常用功能 + /// + /// ListVM中的Model类 + /// ListVM使用的Searcher类 + public class BasePagedListVM : BaseVM, IBasePagedListVM + where TModel : TopBasePoco + where TSearcher : BaseSearcher + { + + + [JsonIgnore] + public string TotalText { get; set; } = CoreProgram._localizer?["Sys.Total"]; + + public virtual DbCommand GetSearchCommand() + { + return null; + } + + private int? _childrenDepth; + + + /// + /// 多级表头深度 默认 1级 + /// + public int GetChildrenDepth() + { + if (_childrenDepth == null) + { + _childrenDepth = _getHeaderDepth(); + } + return _childrenDepth.Value; + } + + /// + /// GridHeaders + /// + [JsonIgnore] + private IEnumerable> GridHeaders { get; set; } + + /// + /// GetHeaders + /// + /// + public IEnumerable> GetHeaders() + { + if (GridHeaders == null) + { + GridHeaders = InitGridHeader(); + } + return GridHeaders; + } + + /// + /// 计算多级表头深度 + /// + /// + private int _getHeaderDepth() + { + IEnumerable> headers = GetHeaders(); + return headers.Max(x => x.MaxDepth); + } + + private List _gridActions; + + /// + /// 页面动作 + /// + public List GetGridActions() + { + if (_gridActions == null) + { + _gridActions = InitGridAction(); + } + return _gridActions; + } + + /// + /// 初始化 InitGridHeader,继承的类应该重载这个函数来设定数据的列和动作 + /// + protected virtual IEnumerable> InitGridHeader() + { + return new List>(); + } + + protected virtual List InitGridAction() + { + return new List(); + } + + #region GenerateExcel + + /// + /// 生成Excel + /// + /// 生成的Excel文件 + public virtual byte[] GenerateExcel() + { + NeedPage = false; + + //获取导出的表头 + if (GridHeaders == null) + { + GetHeaders(); + } + + //去掉ID列和Action列 + RemoveActionAndIdColumn(); + + //如果没有数据源,进行查询 + if (IsSearched == false) + { + DoSearch(); + } + + //获取分成Excel的个数 + ExportMaxCount = ExportMaxCount == 0 ? 1000000 : (ExportMaxCount > 1000000 ? 1000000 : ExportMaxCount); + ExportExcelCount = EntityList.Count < ExportMaxCount ? 1 : ((EntityList.Count % ExportMaxCount) == 0 ? (EntityList.Count / ExportMaxCount) : (EntityList.Count / ExportMaxCount + 1)); + + //如果是1,直接下载Excel,如果是多个,下载ZIP包 + if (ExportExcelCount == 1) + { + return DownLoadExcel(); + } + else + { + return DownLoadZipPackage(typeof(TModel).Name + "_" + DateTime.Now.ToString("yyyyMMddHHmmssffff")); + } + } + + /// + /// 根据集合生成单个Excel + /// + /// + /// + private IWorkbook GenerateWorkBook(List List) + { + IWorkbook book = new XSSFWorkbook(); + ISheet sheet = book.CreateSheet(); + IRow row = sheet.CreateRow(0); + + //创建表头样式 + ICellStyle headerStyle = book.CreateCellStyle(); + headerStyle.FillBackgroundColor = ExportTitleBackColor == null ? HSSFColor.LightBlue.Index : ExportTitleBackColor.Value; + headerStyle.FillPattern = FillPattern.SolidForeground; + headerStyle.FillForegroundColor = ExportTitleBackColor == null ? HSSFColor.LightBlue.Index : ExportTitleBackColor.Value; + headerStyle.BorderBottom = BorderStyle.Thin; + headerStyle.BorderTop = BorderStyle.Thin; + headerStyle.BorderLeft = BorderStyle.Thin; + headerStyle.BorderRight = BorderStyle.Thin; + IFont font = book.CreateFont(); + font.FontName = "Calibri"; + font.FontHeightInPoints = 12; + font.Color = ExportTitleFontColor == null ? HSSFColor.Black.Index : ExportTitleFontColor.Value; + headerStyle.SetFont(font); + + ICellStyle cellStyle = book.CreateCellStyle(); + cellStyle.BorderBottom = BorderStyle.Thin; + cellStyle.BorderTop = BorderStyle.Thin; + cellStyle.BorderLeft = BorderStyle.Thin; + cellStyle.BorderRight = BorderStyle.Thin; + + //生成表头 + int max = MakeExcelHeader(sheet, GridHeaders, 0, 0, headerStyle); + + //放入数据 + var ColIndex = 0; + for (int i = 0; i < List.Count; i++) + { + ColIndex = 0; + var DR = sheet.CreateRow(i + max); + foreach (var baseCol in GridHeaders) + { + foreach (var col in baseCol.BottomChildren) + { + //处理枚举变量的多语言 + bool IsEmunBoolParp = false; + var proType = col.FieldType; + if (proType.IsEnumOrNullableEnum()) + { + IsEmunBoolParp = true; + } //获取数据,并过滤特殊字符 + string text = Regex.Replace(col.GetText(List[i]).ToString(), @"<[^>]*>", String.Empty); + + //处理枚举变量的多语言 + if (IsEmunBoolParp) + { + string enumdisplay = PropertyHelper.GetEnumDisplayName(proType, text); + if (string.IsNullOrEmpty(enumdisplay) == false) + { + text = enumdisplay; + } + + else + { + if (int.TryParse(text, out int enumvalue)) + { + text = PropertyHelper.GetEnumDisplayName(proType, enumvalue); + } + } + } + + //建立excel单元格 + ICell cell; + if (col.FieldType?.IsNumber() == true) + { + cell = DR.CreateCell(ColIndex, CellType.Numeric); + try + { + cell.SetCellValue(Convert.ToDouble(text)); + } + catch { } + } + else + { + cell = DR.CreateCell(ColIndex); + cell.SetCellValue(text); + } + cell.CellStyle = cellStyle; + ColIndex++; + } + } + } + return book; + } + + private byte[] DownLoadExcel() + { + var book = GenerateWorkBook(EntityList); + byte[] rv = new byte[] { }; + using (MemoryStream ms = new MemoryStream()) + { + book.Write(ms); + rv = ms.ToArray(); + } + return rv; + } + + private byte[] DownLoadZipPackage(string FileName) + { + //文件根目录 + string RootPath = $"{Directory.GetCurrentDirectory()}\\{FileName}"; + + //文件夹目录 + string FilePath = $"{RootPath}//FileFolder"; + + //压缩包目录 + string ZipPath = $"{RootPath}//{FileName}.zip"; + + //打开文件夹 + DirectoryInfo FileFolder = new DirectoryInfo(FilePath); + if (!FileFolder.Exists) + { + //创建文件夹 + FileFolder.Create(); + } + else + { + //清空文件夹 + FileSystemInfo[] Files = FileFolder.GetFileSystemInfos(); + foreach (var item in Files) + { + if (item is DirectoryInfo) + { + DirectoryInfo Directory = new DirectoryInfo(item.FullName); + Directory.Delete(true); + } + else + { + File.Delete(item.FullName); + } + } + } + + //放入数据 + for (int i = 0; i < ExportExcelCount; i++) + { + var List = EntityList.Skip(i * ExportMaxCount).Take(ExportMaxCount).ToList(); + var WorkBook = GenerateWorkBook(List); + string SavePath = $"{FilePath}/{FileName}_{i + 1}.xlsx"; + using (FileStream FS = new FileStream(SavePath, FileMode.CreateNew)) + { + WorkBook.Write(FS); + } + } + + //生成压缩包 + ZipFile.CreateFromDirectory(FilePath, ZipPath); + + //读取压缩包 + FileStream ZipFS = new FileStream(ZipPath, FileMode.Open, FileAccess.Read); + byte[] bt = new byte[ZipFS.Length]; + ZipFS.Read(bt, 0, bt.Length); + ZipFS.Close(); + + //删除根目录文件夹 + DirectoryInfo RootFolder = new DirectoryInfo(RootPath); + if (RootFolder.Exists) + { + RootFolder.Delete(true); + } + + return bt; + } + + /// + /// 生成Excel的表头 + /// + /// + /// + /// + /// + /// + /// + private int MakeExcelHeader(ISheet sheet, IEnumerable> cols, int rowIndex, int colIndex, ICellStyle style) + { + var row = sheet.GetRow(rowIndex); + if (row == null) + { + row = sheet.CreateRow(rowIndex); + } + int maxLevel = cols.Select(x => x.MaxLevel).Max(); + //循环所有列 + foreach (var col in cols) + { + //添加新单元格 + var cell = row.CreateCell(colIndex); + cell.CellStyle = style; + cell.SetCellValue(col.Title); + var bcount = col.BottomChildren.Count(); + var rowspan = 0; + if (rowIndex >= 0) + { + rowspan = maxLevel - col.MaxLevel; + } + var cellRangeAddress = new CellRangeAddress(rowIndex, rowIndex + rowspan, colIndex, colIndex + bcount - 1); + sheet.AddMergedRegion(cellRangeAddress); + if (rowspan > 0 || bcount > 1) + { + cell.CellStyle.Alignment = HorizontalAlignment.Center; + cell.CellStyle.VerticalAlignment = VerticalAlignment.Center; + } + for (int i = cellRangeAddress.FirstRow; i <= cellRangeAddress.LastRow; i++) + { + IRow r = CellUtil.GetRow(i, sheet); + for (int j = cellRangeAddress.FirstColumn; j <= cellRangeAddress.LastColumn; j++) + { + ICell c = CellUtil.GetCell(r, (short)j); + c.CellStyle = style; + } + } + if (col.Children != null && col.Children.Count() > 0) + { + MakeExcelHeader(sheet, col.Children, rowIndex + rowspan + 1, colIndex, style); + } + colIndex += bcount; + } + return maxLevel; + } + + #endregion + + #region Old + public SortInfo CreateSortInfo(Expression> pro, SortDir dir) + { + SortInfo rv = new SortInfo + { + Property = PropertyHelper.GetPropertyName(pro), + Direction = dir + }; + return rv; + } + + /// + /// InitList后触发的事件 + /// + public event Action> OnAfterInitList; + + /// + ///记录批量操作时列表中选择的Id + /// + public List Ids { get; set; } + public string SelectorValueField { get; set; } + /// + /// 是否已经搜索过 + /// + [JsonIgnore] + public bool IsSearched { get; set; } + + [JsonIgnore] + public bool PassSearch { get; set; } + /// + /// 查询模式 + /// + [JsonIgnore] + public ListVMSearchModeEnum SearcherMode { get; set; } + + /// + /// 是否需要分页 + /// + [JsonIgnore] + public bool NeedPage { get; set; } + + /// + /// 允许导出Excel的最大行数,超过行数会分成多个文件,最多不能超过100万 + /// + [JsonIgnore] + public int ExportMaxCount { get; set; } + + /// + /// 根据允许导出的Excel最大行数,算出最终导出的Excel个数 + /// + [JsonIgnore] + public int ExportExcelCount { get; set; } + + /// + /// 导出文件第一行背景颜色,使用HSSFColor,例如:HSSFColor.Red.Index + /// + [JsonIgnore] + public short? ExportTitleBackColor { get; set; } + + /// + /// 导出文件第一行文字颜色,使用HSSFColor,例如:HSSFColor.Red.Index + /// + [JsonIgnore] + public short? ExportTitleFontColor { get; set; } + + /// + /// 数据列表 + /// + [JsonIgnore] + public List EntityList { get; set; } + + + /// + /// 搜索条件 + /// + [JsonIgnore] + public TSearcher Searcher { get; set; } + + /// + /// 使用 VM 的 Id 来生成 SearcherDiv 的 Id + /// + [JsonIgnore] + public string SearcherDivId + { + get { return this.UniqueId + "Searcher"; } + } + + + /// + /// 替换查询条件,如果被赋值,则列表会使用里面的Lambda来替换原有Query里面的Where条件 + /// + [JsonIgnore()] + public Expression ReplaceWhere { get; set; } + + /// + /// 构造函数 + /// + public BasePagedListVM() + { + //默认需要分页 + NeedPage = true; + //初始化数据列表 + EntityList = new List(); + //初始化搜索条件 + Searcher = typeof(TSearcher).GetConstructor(Type.EmptyTypes).Invoke(null) as TSearcher; + } + + /// + /// 获取数据列表 + /// + /// 数据列表 + public IEnumerable GetEntityList() + { + if (IsSearched == false && (EntityList == null || EntityList.Count == 0)) + { + DoSearch(); + } + return EntityList?.AsEnumerable(); + } + + + /// + /// 调用InitListVM并触发OnAfterInitList事件 + /// + public void DoInitListVM() + { + InitListVM(); + OnAfterInitList?.Invoke(this); + } + + + /// + /// 初始化ListVM,继承的类应该重载这个函数来设定数据的列和动作 + /// + protected virtual void InitListVM() + { + } + + public virtual bool GetIsSelected(object item) + { + return false; + } + + public override void Validate() + { + Searcher?.Validate(); + base.Validate(); + } + + /// + /// 设定行前景色,继承的类应重载这个函数来根据每行的数据显示不同的前景色 + /// + /// 数据 + /// 前景颜色 + public virtual string SetFullRowColor(object entity) + { + return ""; + } + + /// + /// 设定行背景色,继承的类应重载这个函数来根据每行的数据显示不同的背景色 + /// + /// 数据 + /// 背景颜色 + public virtual string SetFullRowBgColor(object entity) + { + return ""; + } + + /// + /// 设定搜索语句,继承的类应该重载这个函数来指定自己的搜索语句 + /// + /// 搜索语句 + public virtual IOrderedQueryable GetSearchQuery() + { + return DC.Set().OrderByDescending(x => x.ID); + } + + /// + /// 设定导出时搜索语句,继承的类应该重载这个函数来指定自己导出时的搜索语句,如不指定则默认和搜索用的搜索语句相同 + /// + /// 搜索语句 + public virtual IOrderedQueryable GetExportQuery() + { + return GetSearchQuery(); + } + + /// + /// 设定搜索语句,继承的类应该重载这个函数来指定自己导出时的搜索语句,如不指定则默认和搜索用的搜索语句相同 + /// + /// 搜索语句 + public virtual IOrderedQueryable GetSelectorQuery() + { + return GetSearchQuery(); + } + + /// + /// 设定勾选后导出的搜索语句,继承的类应该重载这个函数来指定自己导出时的搜索语句,如不指定则默认和搜索用的搜索语句相同 + /// + /// 搜索语句 + public virtual IOrderedQueryable GetCheckedExportQuery() + { + var baseQuery = GetBatchQuery(); + return baseQuery; + } + + /// + /// 设定批量模式下的搜索语句,继承的类应重载这个函数来指定自己批量模式的搜索语句,如果不指定则默认使用Ids.Contains(x.Id)来代替搜索语句中的Where条件 + /// + /// 搜索语句 + public virtual IOrderedQueryable GetBatchQuery() + { + var baseQuery = GetSearchQuery(); + if (ReplaceWhere == null) + { + var mod = new WhereReplaceModifier(Ids.GetContainIdExpression()); + var newExp = mod.Modify(baseQuery.Expression); + var newQuery = baseQuery.Provider.CreateQuery(newExp) as IOrderedQueryable; + return newQuery; + } + else + { + return baseQuery; + } + } + + /// + /// 设定主从模式的搜索语句,继承的类应该重载这个函数来指定自己主从模式的搜索语句,如不指定则默认和搜索用的搜索语句相同 + /// + /// 搜索语句 + public virtual IOrderedQueryable GetMasterDetailsQuery() + { + return GetSearchQuery(); + } + + /// + /// 进行搜索 + /// + public virtual void DoSearch() + { + var cmd = GetSearchCommand(); + if (cmd == null) + { + IOrderedQueryable query = null; + //根据搜索模式调用不同的函数 + switch (SearcherMode) + { + case ListVMSearchModeEnum.Search: + query = GetSearchQuery(); + break; + case ListVMSearchModeEnum.Export: + query = GetExportQuery(); + break; + case ListVMSearchModeEnum.Batch: + query = GetBatchQuery(); + break; + case ListVMSearchModeEnum.MasterDetail: + query = GetMasterDetailsQuery(); + break; + case ListVMSearchModeEnum.CheckExport: + query = GetCheckedExportQuery(); + break; + case ListVMSearchModeEnum.Selector: + query = GetSelectorQuery(); + break; + default: + query = GetSearchQuery(); + break; + } + + //如果设定了替换条件,则使用替换条件替换Query中的Where语句 + if (ReplaceWhere != null) + { + var mod = new WhereReplaceModifier(ReplaceWhere as Expression>); + var newExp = mod.Modify(query.Expression); + query = query.Provider.CreateQuery(newExp) as IOrderedQueryable; + } + if (Searcher.SortInfo != null) + { + var mod = new OrderReplaceModifier(Searcher.SortInfo); + var newExp = mod.Modify(query.Expression); + query = query.Provider.CreateQuery(newExp) as IOrderedQueryable; + } + if (typeof(IPersistPoco).IsAssignableFrom( typeof(TModel))) + { + var mod = new IsValidModifier(); + var newExp = mod.Modify(query.Expression); + query = query.Provider.CreateQuery(newExp) as IOrderedQueryable; + } + if (PassSearch == false) + { + //如果需要分页,则添加分页语句 + if (NeedPage && Searcher.Limit != -1) + { + //获取返回数据的数量 + var count = query.Count(); + if (count < 0) + { + count = 0; + } + if (Searcher.Limit == 0) + { + Searcher.Limit = ConfigInfo?.UIOptions.DataTable.RPP ?? 20; + } + //根据返回数据的数量,以及预先设定的每页行数来设定数据量和总页数 + Searcher.Count = count; + Searcher.PageCount = (int)Math.Ceiling((1.0 * Searcher.Count / Searcher.Limit)); + if (Searcher.Page <= 0) + { + Searcher.Page = 1; + } + if (Searcher.PageCount > 0 && Searcher.Page > Searcher.PageCount) + { + Searcher.Page = Searcher.PageCount; + } + EntityList = query.Skip((Searcher.Page - 1) * Searcher.Limit).Take(Searcher.Limit).AsNoTracking().ToList(); + } + else //如果不需要分页则直接获取数据 + { + EntityList = query.AsNoTracking().ToList(); + Searcher.Count = EntityList.Count(); + Searcher.Limit = EntityList.Count(); + Searcher.PageCount = 1; + Searcher.Page = 1; + } + } + else + { + EntityList = query.AsNoTracking().ToList(); + } + } + else + { + ProcessCommand(cmd); + } + IsSearched = true; + //调用AfterDoSearch函数来处理自定义的后续操作 + AfterDoSearcher(); + } + + + private void ProcessCommand(DbCommand cmd) + { + object total; + + if (Searcher.Page <= 0) + { + Searcher.Page = 1; + } + if (DC.Database.IsMySql()) + { + List parms = new List(); + foreach (MySqlParameter item in cmd.Parameters) + { + parms.Add(new MySqlParameter(string.Format("@{0}", item.ParameterName), item.Value)); + } + if (cmd.CommandType == CommandType.StoredProcedure) + { + parms.Add(new MySqlParameter("@SearchMode", Enum.GetName(typeof(ListVMSearchModeEnum), SearcherMode))); + parms.Add(new MySqlParameter("@NeedPage", (NeedPage && Searcher.Limit != -1))); + parms.Add(new MySqlParameter("@CurrentPage", Searcher.Page)); + parms.Add(new MySqlParameter("@RecordsPerPage", Searcher.Limit)); + parms.Add(new MySqlParameter("@Sort", Searcher.SortInfo?.Property)); + parms.Add(new MySqlParameter("@SortDir", Searcher.SortInfo?.Direction)); + parms.Add(new MySqlParameter("@IDs", Ids == null ? "" : Ids.ToSepratedString())); + + MySqlParameter outp = new MySqlParameter("@TotalRecords", MySqlDbType.Int64) + { + Value = 0, + Direction = ParameterDirection.Output + }; + parms.Add(outp); + } + var pa = parms.ToArray(); + + EntityList = DC.Run(cmd.CommandText, cmd.CommandType, pa).ToList(); + if (cmd.CommandType == CommandType.StoredProcedure) + { + total = pa.Last().Value; + } + else + { + total = EntityList.Count; + } + } + else if (DC.Database.IsNpgsql()) + { + List parms = new List(); + foreach (NpgsqlParameter item in cmd.Parameters) + { + parms.Add(new NpgsqlParameter(string.Format("@{0}", item.ParameterName), item.Value)); + } + + if (cmd.CommandType == CommandType.StoredProcedure) + { + parms.Add(new NpgsqlParameter("@SearchMode", Enum.GetName(typeof(ListVMSearchModeEnum), SearcherMode))); + parms.Add(new NpgsqlParameter("@NeedPage", (NeedPage && Searcher.Limit != -1))); + parms.Add(new NpgsqlParameter("@CurrentPage", Searcher.Page)); + parms.Add(new NpgsqlParameter("@RecordsPerPage", Searcher.Limit)); + parms.Add(new NpgsqlParameter("@Sort", Searcher.SortInfo?.Property)); + parms.Add(new NpgsqlParameter("@SortDir", Searcher.SortInfo?.Direction)); + parms.Add(new NpgsqlParameter("@IDs", Ids == null ? "" : Ids.ToSepratedString())); + + NpgsqlParameter outp = new NpgsqlParameter("@TotalRecords", NpgsqlDbType.Bigint) + { + Value = 0, + Direction = ParameterDirection.Output + }; + parms.Add(outp); + } + var pa = parms.ToArray(); + + EntityList = DC.Run(cmd.CommandText, cmd.CommandType, pa).ToList(); + if (cmd.CommandType == CommandType.StoredProcedure) + { + total = pa.Last().Value; + } + else + { + total = EntityList.Count; + } + } + else + { + List parms = new List(); + foreach (SqlParameter item in cmd.Parameters) + { + parms.Add(new SqlParameter(string.Format("@{0}", item.ParameterName), item.Value)); + } + if (cmd.CommandType == CommandType.StoredProcedure) + { + + parms.Add(new SqlParameter("@SearchMode", Enum.GetName(typeof(ListVMSearchModeEnum), SearcherMode))); + parms.Add(new SqlParameter("@NeedPage", (NeedPage && Searcher.Limit != -1))); + parms.Add(new SqlParameter("@CurrentPage", Searcher.Page)); + parms.Add(new SqlParameter("@RecordsPerPage", Searcher.Limit)); + parms.Add(new SqlParameter("@Sort", Searcher.SortInfo?.Property)); + parms.Add(new SqlParameter("@SortDir", Searcher.SortInfo?.Direction)); + parms.Add(new SqlParameter("@IDs", Ids == null ? "" : Ids.ToSepratedString())); + + SqlParameter outp = new SqlParameter("@TotalRecords", 0) + { + Direction = ParameterDirection.Output + }; + parms.Add(outp); + } + var pa = parms.ToArray(); + + EntityList = DC.Run(cmd.CommandText, cmd.CommandType, pa).ToList(); + if (cmd.CommandType == CommandType.StoredProcedure) + { + total = pa.Last().Value; + } + else + { + total = EntityList.Count; + } + + } + if (NeedPage && Searcher.Limit != -1) + { + if (total != null) + { + try + { + Searcher.Count = long.Parse(total.ToString()); + Searcher.PageCount = (int)((Searcher.Count - 1) / Searcher.Limit + 1); + } + catch { } + } + } + else + { + Searcher.PageCount = EntityList.Count; + } + + } + + public DateTime AddTime(DateTime dt, string type, int size) + { + switch (type) + { + case "year": + return dt.AddYears(size); + case "month": + return dt.AddMonths(size); + case "day": + return dt.AddDays(size); + case "hour": + return dt.AddHours(size); + case "minute": + return dt.AddMinutes(size); + case "second": + return dt.AddSeconds(size); + default: + return dt; + } + } + + /// + /// 搜索后运行的函数,继承的类如果需要在搜索结束后进行其他操作,可重载这个函数 + /// + public virtual void AfterDoSearcher() + { + if (SearcherMode == ListVMSearchModeEnum.Selector && Ids != null && Ids.Count > 0 && EntityList != null && EntityList.Count > 0) + { + foreach (var item in EntityList) + { + if (string.IsNullOrEmpty(SelectorValueField) || SelectorValueField.ToLower() == "id") + { + var id = item.GetID(); + if (Ids.Contains(id.ToString())) + { + item.Checked = true; + } + } + else + { + var v = item.GetPropertyValue(SelectorValueField); + if (Ids.Contains(v.ToString())) + { + item.Checked = true; + } + } + } + } + } + + /// + /// 删除所有ActionGridColumn的列 + /// + public void RemoveActionColumn(object root = null) + { + if (root == null) + { + if (GridHeaders == null) + { + GetHeaders(); + } + root = GridHeaders; + } + if (root != null) + { + //IEnumerable> + var aroot = root as List>; + var toRemove = aroot.Where(x => x.ColumnType == GridColumnTypeEnum.Action).FirstOrDefault(); + aroot.Remove(toRemove); + foreach (var child in aroot) + { + if (child.Children != null && child.Children.Count() > 0) + { + RemoveActionColumn(child.Children); + } + } + } + } + + public void RemoveAction() + { + _gridActions = new List(); + } + + public void RemoveActionAndIdColumn(IEnumerable> root = null) + { + if (root == null) + { + if (GridHeaders == null) + { + GetHeaders(); + } + root = GridHeaders; + } + if (root != null) + { + var aroot = root as List>; + List> remove = null; + var idpro = typeof(TModel).GetSingleProperty("ID")?.PropertyType; + if (idpro == typeof(string)) + { + remove = aroot.Where(x => x.ColumnType == GridColumnTypeEnum.Action || x.Hide == true || x.DisableExport).ToList(); + } + else + { + remove = aroot.Where(x => x.ColumnType == GridColumnTypeEnum.Action || x.Hide == true || x.DisableExport || x.FieldName?.ToLower() == "id").ToList(); + } + foreach (var item in remove) + { + aroot.Remove(item); + } + foreach (var child in root) + { + if (child.Children != null && child.Children.Count() > 0) + { + RemoveActionAndIdColumn(child.Children); + } + } + } + } + + + /// + /// 添加Error列,主要为批量模式使用 + /// + public void AddErrorColumn() + { + GetHeaders(); + //寻找所有Header为错误信息的列,如果没有则添加 + if (GridHeaders.Where(x => x.Field == "BatchError").FirstOrDefault() == null) + { + var temp = GridHeaders as List>; + if (temp.Where(x => x.ColumnType == GridColumnTypeEnum.Action).FirstOrDefault() == null) + { + temp.Add(this.MakeGridColumn(x => x.BatchError, Width: 200, Header: Core.CoreProgram._localizer?["Sys.Error"]).SetForeGroundFunc(x => "ff0000").SetFixed(GridColumnFixedEnum.Right)); + } + else + { + temp.Insert(temp.Count - 1, this.MakeGridColumn(x => x.BatchError, Width: 200, Header: Core.CoreProgram._localizer?["Sys.Error"]).SetForeGroundFunc(x => "ff0000").SetFixed(GridColumnFixedEnum.Right)); + } + } + } + + public void ProcessListError(List Entities) + { + if(Entities == null) + { + return; + } + EntityList = Entities; + IsSearched = true; + bool haserror = false; + List keys = new List(); + if (string.IsNullOrEmpty(DetailGridPrix) == false) + { + if (EntityList.Any(x => x.BatchError != null)) + { + haserror = true; + } + else + { + foreach (var item in MSD.Keys) + { + if (item.StartsWith(DetailGridPrix+"[")) + { + var errors = MSD[item]; + if (errors.Count > 0) + { + Regex r = new Regex($"{DetailGridPrix}\\[(.*?)\\]"); + try + { + if (int.TryParse(r.Match(item).Groups[1].Value, out int index)) + { + EntityList[index].BatchError = errors.Select(x => x.ErrorMessage).ToSepratedString(); + keys.Add(item); + haserror = true; + } + } + catch { } + } + } + } + foreach (var item in keys) + { + MSD.RemoveModelError(item); + } + } + if (haserror) + { + AddErrorColumn(); + } + } + } + + public TModel CreateEmptyEntity() + { + return typeof(TModel).GetConstructor(Type.EmptyTypes).Invoke(null) as TModel; + } + + public void ClearEntityList() + { + EntityList?.Clear(); + } + + public string DetailGridPrix { get; set; } + + public Type ModelType => typeof(TModel); + + #endregion + + public virtual void UpdateEntityList(bool updateAllFields = false) + { + if (EntityList != null) + { + var ftype = EntityList.GetType().GenericTypeArguments.First(); + var itemPros = ftype.GetAllProperties(); + + foreach (var newitem in EntityList) + { + var subtype = newitem.GetType(); + if (typeof(IBasePoco).IsAssignableFrom( subtype)) + { + IBasePoco ent = newitem as IBasePoco; + if (ent.UpdateTime == null) + { + ent.UpdateTime = DateTime.Now; + } + if (string.IsNullOrEmpty(ent.UpdateBy)) + { + ent.UpdateBy = LoginUserInfo?.ITCode; + } + } + //循环页面传过来的子表数据,将关联到TopBasePoco的字段设为null,并且把外键字段的值设定为主表ID + foreach (var itempro in itemPros) + { + if (itempro.PropertyType.IsSubclassOf(typeof(TopBasePoco))) + { + itempro.SetValue(newitem, null); + } + } + } + + IEnumerable data = null; + //打开新的数据库联接,获取数据库中的主表和子表数据 + using (var ndc = DC.CreateNew()) + { + var ids = EntityList.Select(x => x.GetID().ToString()).ToList(); + data = ndc.Set().AsNoTracking().Where(ids.GetContainIdExpression()).ToList(); + } + //比较子表原数据和新数据的区别 + IEnumerable toadd = null; + IEnumerable toremove = null; + Utils.CheckDifference(data, EntityList, out toremove, out toadd); + //设定子表应该更新的字段 + List setnames = new List(); + foreach (var field in FC.Keys) + { + if (field.StartsWith("EntityList[0].")) + { + string name = field.Replace("EntityList[0].", ""); + setnames.Add(name); + } + } + + //前台传过来的数据 + foreach (var newitem in EntityList) + { + //数据库中的数据 + foreach (var item in data) + { + //需要更新的数据 + if (newitem.GetID().ToString() == item.GetID().ToString()) + { + dynamic i = newitem; + var newitemType = item.GetType(); + foreach (var itempro in itemPros) + { + if (!itempro.PropertyType.IsSubclassOf(typeof(TopBasePoco)) && (updateAllFields == true || setnames.Contains(itempro.Name))) + { + var notmapped = itempro.GetCustomAttribute(); + if (itempro.Name != "ID" && notmapped == null && itempro.PropertyType.IsList() == false) + { + DC.UpdateProperty(i, itempro.Name); + } + } + } + if ( typeof(IBasePoco).IsAssignableFrom( item.GetType())) + { + DC.UpdateProperty(i, "UpdateTime"); + DC.UpdateProperty(i, "UpdateBy"); + } + } + } + } + //需要删除的数据 + foreach (var item in toremove) + { + //如果是PersistPoco,则把IsValid设为false,并不进行物理删除 + if (typeof(IPersistPoco).IsAssignableFrom( ftype)) + { + (item as IPersistPoco).IsValid = false; + if (typeof(IBasePoco).IsAssignableFrom(ftype)) + { + (item as IBasePoco).UpdateTime = DateTime.Now; + (item as IBasePoco).UpdateBy = LoginUserInfo?.ITCode; + } + dynamic i = item; + DC.UpdateEntity(i); + } + else + { + foreach (var itempro in itemPros) + { + if (itempro.PropertyType.IsSubclassOf(typeof(TopBasePoco))) + { + itempro.SetValue(item, null); + } + } + dynamic i = item; + DC.DeleteEntity(i); + } + } + //需要添加的数据 + foreach (var item in toadd) + { + if (typeof(IBasePoco).IsAssignableFrom( item.GetType())) + { + IBasePoco ent = item as IBasePoco; + if (ent.CreateTime == null) + { + ent.CreateTime = DateTime.Now; + } + if (string.IsNullOrEmpty(ent.CreateBy)) + { + ent.CreateBy = LoginUserInfo?.ITCode; + } + } + DC.AddEntity(item); + + + } + + DC.SaveChanges(); + } + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BaseSearcher.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BaseSearcher.cs new file mode 100644 index 0000000..1ccc8ab --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BaseSearcher.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; +using WalkingTec.Mvvm.Core.Extensions; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// 搜索条件基类,一般和ListVM配合使用实现对ListVM的搜索功能。 + /// + public class BaseSearcher : ISearcher + { + #region Property + + #region 分页相关 + /// + /// 当前页 + /// + public int Page { get; set; } + /// + /// 每页数 + /// + public int Limit { get; set; } + /// + /// 记录数 + /// + [JsonIgnore] + public long Count { get; set; } + /// + /// 分页数 + /// + [JsonIgnore] + public int PageCount { get; set; } + #endregion + + /// + /// 记录 Controller 中的表单数据 + /// + [JsonIgnore] + public Dictionary FC { get; set; } + + [JsonIgnore] + public IModelStateService MSD { get => Wtm?.MSD; } + + /// + /// 获取VM的全名 + /// + [JsonIgnore] + public string VMFullName + { + get + { + var name = GetType().AssemblyQualifiedName; + name = name.Substring(0, name.LastIndexOf(", Version=")); + return name; + } + } + + private IDataContext _dc; + /// + /// 数据库环境 + /// + [JsonIgnore] + public IDataContext DC + { + get + { + if (_dc == null) + { + return Wtm?.DC; + } + else + { + return _dc; + } + } + set + { + _dc = value; + } + } + + /// + /// Session信息 + /// + [JsonIgnore] + public ISessionService Session { get => Wtm?.Session; } + + /// + /// 当前登录人信息 + /// + [JsonIgnore] + public LoginUserInfo LoginUserInfo { get => Wtm?.LoginUserInfo; } + + [JsonIgnore] + public string ViewDivId { get; set; } + #region 未使用 + /// + /// 排序信息 + /// + public SortInfo SortInfo { get; set; } + + /// + /// 前台搜索框是否展开 + /// + [JsonIgnore] + public bool? IsExpanded { get; set; } + + private Guid _uniqueId; + [JsonIgnore] + public string UniqueId + { + get + { + if (_uniqueId == Guid.Empty) + { + _uniqueId = Guid.NewGuid(); + } + return _uniqueId.ToNoSplitString(); + } + } + + [JsonIgnore] + public WTMContext Wtm { get; set; } + #endregion + + #endregion + + #region Event + + /// + /// InitVM 完成后触发的事件 + /// + public event Action OnAfterInit; + /// + /// ReInitVM 完成后触发的事件 + /// + public event Action OnAfterReInit; + + #endregion + + #region Method + + /// + /// 调用 InitVM 并触发 OnAfterInit 事件 + /// + public void DoInit() + { + InitVM(); + OnAfterInit?.Invoke(this); + } + + /// + /// 调用 ReInitVM 并触发 OnAfterReInit 事件 + /// + public void DoReInit() + { + ReInitVM(); + OnAfterReInit?.Invoke(this); + } + + /// + /// 初始化ViewModel,框架会在创建VM实例之后自动调用本函数 + /// + protected virtual void InitVM() + { + } + + /// + /// 从新初始化ViewModel,框架会在验证失败时自动调用本函数 + /// + protected virtual void ReInitVM() + { + InitVM(); + } + + public virtual void Validate() + { + + } + /// + /// 将源 VM 的 FC 等内容复制到本VM中 + /// + /// + public void CopyContext(IBaseVM vm) + { + FC = vm.FC; + this.Wtm = vm.Wtm; + this.ViewDivId = vm.ViewDivId; + } + + #endregion + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BaseTemplateVM.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BaseTemplateVM.cs new file mode 100644 index 0000000..539ae64 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BaseTemplateVM.cs @@ -0,0 +1,345 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Data; +using System.IO; +using System.Linq; +using NPOI.HSSF.UserModel; +using NPOI.HSSF.Util; +using NPOI.SS.UserModel; +using NPOI.XSSF.UserModel; + +namespace WalkingTec.Mvvm.Core +{ + public class BaseTemplateVM : BaseVM + { + #region 属性 + /// + /// 下载模板显示名称 + /// + public string FileDisplayName { get; set; } + + /// + /// 是否验证模板类型(当其他系统模板导入到某模块时可设置为False) + /// + public bool ValidityTemplateType { get; set; } + + /// + /// 需要导出的数据 + /// + public DataTable TemplateDataTable { get; set; } + + /// + /// 下载模版页面参数 + /// + public Dictionary Parms { get; set; } + + /// + /// Excel索引 + /// + public long ExcelIndex { get; set; } + #endregion + + #region 构造函数 + public BaseTemplateVM() + { + ValidityTemplateType = true; + Parms = new Dictionary(); + var propetys = this.GetType().GetFields().Where(x => x.FieldType == typeof(ExcelPropety)).ToList(); + for (int porpetyIndex = 0; porpetyIndex < propetys.Count(); porpetyIndex++) + { + ExcelPropety excelPropety = (ExcelPropety)propetys[porpetyIndex].GetValue(this); + if (propetys[porpetyIndex].GetCustomAttributes(typeof(DisplayAttribute), false).Length == 0) + { + excelPropety.ColumnName = excelPropety.FieldDisplayName; + } + else + { + excelPropety.ColumnName = propetys[porpetyIndex].GetPropertyDisplayName(); + } + } + } + #endregion + + #region 初始化Excel属性数据 + /// + /// 初始化Excel属性数据 包括动态列,列表中的下拉选项 + /// + public virtual void InitExcelData() + { + + } + + public virtual void InitCustomFormat() + { + + } + + #endregion + + #region 初始化模版数据 + /// + /// 初始化模版数据 + /// + public virtual void SetTemplateDataValus() + { + + } + #endregion + + #region 生成模板 + /// + /// 生成模板 + /// + /// 文件名 + /// 生成的模版文件 + public byte[] GenerateTemplate(out string displayName) + { + //设置导出的文件名称 + string SheetName = !string.IsNullOrEmpty(FileDisplayName) ? FileDisplayName : this.GetType().Name; + displayName = SheetName + "_" + DateTime.Now.ToString("yyyy-MM-dd") + "_" + DateTime.Now.ToString("hh^mm^ss") + ".xlsx"; + + //1.声明Excel文档 + IWorkbook workbook = new XSSFWorkbook(); + + //加载初始化数据和下拉菜单数据,可重载 + InitExcelData(); + + //设置TemplateDataTable的各列的类型 + CreateDataTable(); + + //设置初始化数据到DataTable中 + SetTemplateDataValus(); + + //2.设置workbook的sheet页 + ISheet sheet = workbook.CreateSheet(); + workbook.SetSheetName(0, SheetName); + + //3.设置Sheet页的Row + IRow row = sheet.CreateRow(0); + row.HeightInPoints = 20; + + ISheet enumSheet = workbook.CreateSheet(); + IRow enumSheetRow1 = enumSheet.CreateRow(0); + enumSheetRow1.CreateCell(0).SetCellValue(CoreProgram._localizer?["Sys.Yes"]); + enumSheetRow1.CreateCell(1).SetCellValue(CoreProgram._localizer?["Sys.No"]); + enumSheetRow1.CreateCell(2).SetCellValue(this.GetType().Name); //为模板添加标记,必要时可添加版本号 + + ISheet dataSheet = workbook.CreateSheet(); + + #region 设置excel模板列头 + //默认灰色 + var headerStyle = GetCellStyle(workbook); + headerStyle.IsLocked = true; + + //黄色 + var yellowStyle = GetCellStyle(workbook, BackgroudColorEnum.Yellow); + yellowStyle.IsLocked = true; + + //红色 + var redStyle = GetCellStyle(workbook, BackgroudColorEnum.Red); + redStyle.IsLocked = true; + + //取得所有ExcelPropety + var propetys = this.GetType().GetFields().Where(x => x.FieldType == typeof(ExcelPropety)).ToList(); + + //设置列的索引 + int _currentColunmIndex = 0; + + //设置Excel是否需要保护,默认不保护 + bool IsProtect = false; + + //循环类的属性,赋值给列 + for (int porpetyIndex = 0; porpetyIndex < propetys.Count(); porpetyIndex++) + { + //依次获取属性字段 + ExcelPropety excelPropety = (ExcelPropety)propetys[porpetyIndex].GetValue(this); + ColumnDataType dateType = (excelPropety.DataType == ColumnDataType.DateTime || excelPropety.DataType == ColumnDataType.Date) ? ColumnDataType.Text : excelPropety.DataType; //日期类型默认设置成Text类型,在赋值时会进行日期验证 + + //设置是否保护Excel + if (excelPropety.ReadOnly) + { + IsProtect = true; + } + //给必填项加星号 + string colName = excelPropety.IsNullAble ? excelPropety.ColumnName : excelPropety.ColumnName + "*"; + row.CreateCell(_currentColunmIndex).SetCellValue(colName); + + //修改列头样式 + switch (excelPropety.BackgroudColor) + { + case BackgroudColorEnum.Yellow: + row.Cells[_currentColunmIndex].CellStyle = yellowStyle; + break; + case BackgroudColorEnum.Red: + row.Cells[_currentColunmIndex].CellStyle = redStyle; + break; + default: + row.Cells[_currentColunmIndex].CellStyle = headerStyle; + break; + } + + var dataStyle = workbook.CreateCellStyle(); + var dataFormat = workbook.CreateDataFormat(); + + if (dateType == ColumnDataType.Dynamic) + { + int dynamicColCount = excelPropety.DynamicColumns.Count(); + for (int dynamicColIndex = 0; dynamicColIndex < dynamicColCount; dynamicColIndex++) + { + var dynamicCol = excelPropety.DynamicColumns.ToList()[dynamicColIndex]; + string dynamicColName = excelPropety.IsNullAble ? dynamicCol.ColumnName : dynamicCol.ColumnName + "*"; + row.CreateCell(_currentColunmIndex).SetCellValue(dynamicColName); + row.Cells[_currentColunmIndex].CellStyle = headerStyle; + if (dynamicCol.ReadOnly) + { + IsProtect = true; + } + //设定列宽 + if (excelPropety.CharCount > 0) + { + sheet.SetColumnWidth(_currentColunmIndex, excelPropety.CharCount * 256); + dataStyle.WrapText = true; + } + else + { + sheet.AutoSizeColumn(_currentColunmIndex); + } + //设置单元格样式及数据类型 + dataStyle.IsLocked = excelPropety.ReadOnly; + dynamicCol.SetColumnFormat(dynamicCol.DataType, _currentColunmIndex, sheet, dataSheet, dataStyle, dataFormat); + _currentColunmIndex++; + } + } + else + { + //设定列宽 + if (excelPropety.CharCount > 0) + { + sheet.SetColumnWidth(_currentColunmIndex, excelPropety.CharCount * 256); + dataStyle.WrapText = true; + } + else + { + sheet.AutoSizeColumn(_currentColunmIndex); + } + //设置是否锁定 + dataStyle.IsLocked = excelPropety.ReadOnly; + //设置单元格样式及数据类型 + excelPropety.SetColumnFormat(dateType, _currentColunmIndex, sheet, dataSheet, dataStyle, dataFormat); + _currentColunmIndex++; + } + } + #endregion + + #region 添加模版数据 + if (TemplateDataTable.Rows.Count > 0) + { + for (int i = 0; i < TemplateDataTable.Rows.Count; i++) + { + DataRow tableRow = TemplateDataTable.Rows[i]; + IRow dataRow = sheet.CreateRow(1 + i); + for (int porpetyIndex = 0; porpetyIndex < propetys.Count(); porpetyIndex++) + { + string colName = propetys[porpetyIndex].Name; + tableRow[colName].ToString(); + dataRow.CreateCell(porpetyIndex).SetCellValue(tableRow[colName].ToString()); + } + } + } + #endregion + + //冻结行 + sheet.CreateFreezePane(0, 1, 0, 1); + + //锁定excel + if (IsProtect) + { + sheet.ProtectSheet("password"); + } + + //隐藏前2个Sheet + workbook.SetSheetHidden(1, SheetState.Hidden); + workbook.SetSheetHidden(2, SheetState.Hidden); + + //返回byte数组 + MemoryStream ms = new MemoryStream(); + workbook.Write(ms); + return ms.ToArray(); + } + #endregion + + #region 取得表头的样式 + private static ICellStyle GetCellStyle(IWorkbook workbook, BackgroudColorEnum backgroudColor = BackgroudColorEnum.Grey) + { + var headerStyle = workbook.CreateCellStyle(); + + //设定表头样式 + headerStyle.BorderBottom = BorderStyle.Thin; + headerStyle.BorderLeft = BorderStyle.Thin; + headerStyle.BorderRight = BorderStyle.Thin; + headerStyle.BorderTop = BorderStyle.Thin; + + //用灰色填充背景 + short headerbg; + + switch (backgroudColor) + { + case BackgroudColorEnum.Grey: + headerbg = HSSFColor.LightBlue.Index; + break; + case BackgroudColorEnum.Yellow: + headerbg = HSSFColor.LightYellow.Index; + break; + case BackgroudColorEnum.Red: + headerbg = HSSFColor.Pink.Index; + break; + default: + headerbg = HSSFColor.Pink.Index; + break; + } + + headerStyle.FillForegroundColor = headerbg; + headerStyle.FillPattern = FillPattern.SolidForeground; + headerStyle.FillBackgroundColor = headerbg; + headerStyle.Alignment = HorizontalAlignment.Center; + return headerStyle; + } + #endregion + + #region 初始化DataTable(不含动态列) + private void CreateDataTable() + { + TemplateDataTable = new DataTable(); + var propetys = this.GetType().GetFields().Where(x => x.FieldType == typeof(ExcelPropety)).ToList(); + foreach (var p in propetys) + { + ExcelPropety excelPropety = (ExcelPropety)p.GetValue(this); + ColumnDataType dateType = excelPropety.DataType; + switch (dateType) + { + case ColumnDataType.Bool: + TemplateDataTable.Columns.Add(p.Name, typeof(bool)); + break; + case ColumnDataType.Date: + TemplateDataTable.Columns.Add(p.Name, typeof(string)); + break; + case ColumnDataType.Number: + TemplateDataTable.Columns.Add(p.Name, typeof(int)); + break; + case ColumnDataType.Text: + TemplateDataTable.Columns.Add(p.Name, typeof(string)); + break; + case ColumnDataType.Float: + TemplateDataTable.Columns.Add(p.Name, typeof(decimal)); + break; + default: + TemplateDataTable.Columns.Add(p.Name, typeof(string)); + break; + } + } + } + #endregion + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BaseVM.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BaseVM.cs new file mode 100644 index 0000000..fc6635b --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/BaseVM.cs @@ -0,0 +1,284 @@ + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; + +using WalkingTec.Mvvm.Core.Extensions; +using WalkingTec.Mvvm.Core.Support.Json; + +namespace WalkingTec.Mvvm.Core +{ + + /// + /// 所有ViewModel的基类,提供了基本的功能 + /// + public class BaseVM : IBaseVM + { + /// + /// BaseVM + /// + public BaseVM() + { + FC = new Dictionary(); + } + + #region Property + + [JsonIgnore] + public WTMContext Wtm { get; set; } + + private Guid _uniqueId; + /// + /// VM实例的Id + /// + [JsonIgnore] + public string UniqueId + { + get + { + if (_uniqueId == Guid.Empty) + { + _uniqueId = Guid.NewGuid(); + } + return _uniqueId.ToNoSplitString(); + } + } + + + /// + /// 前台传递过来的弹出窗口ID,多层弹出窗口用逗号分隔 + /// + [JsonIgnore] + public string WindowIds { get => Wtm?.WindowIds; } + + private string _viewdivid; + /// + /// PartialView中主Div的Id + /// + [JsonIgnore] + public string ViewDivId + { + set { _viewdivid = value; } + get + { + if (string.IsNullOrEmpty(_viewdivid)) + { + _viewdivid = "ViewDiv" + UniqueId; + } + return _viewdivid; + } + } + + + private IDataContext _dc; + /// + /// 数据库环境 + /// + [JsonIgnore] + public IDataContext DC + { + get + { + if (_dc == null) + { + return Wtm?.DC; + } + else + { + return _dc; + } + } + set + { + _dc = value; + } + } + + /// + /// 获取VM的全名 + /// + [JsonIgnore] + public string VMFullName + { + get + { + var name = GetType().AssemblyQualifiedName; + name = name.Substring(0, name.LastIndexOf(", Version=")); + return name; + } + } + + /// + /// 获取VM所在Dll + /// + [JsonIgnore] + public string CreatorAssembly + { + get; set; + } + + /// + /// 获取当前使用的连接字符串 + /// + [JsonIgnore] + public string CurrentCS { get => Wtm?.CurrentCS; } + + /// + /// 记录Controller中传递过来的表单数据 + /// + [JsonIgnore] + public Dictionary FC { get; set; } + + /// + /// 获取配置文件的信息 + /// + [JsonIgnore] + public Configs ConfigInfo { get=> Wtm?.ConfigInfo; } + + + [JsonIgnore] + public IUIService UIService { get=> Wtm?.UIService; } + + /// + /// 当前弹出层ID + /// + [JsonIgnore] + public string CurrentWindowId { get => Wtm?.CurrentWindowId; } + + /// + /// 父级弹出层ID + /// + [JsonIgnore] + public string ParentWindowId { get => Wtm?.ParentWindowId; } + + [JsonIgnore] + public IDistributedCache Cache { get => Wtm?.Cache; } + + /// + /// 当前登录人信息 + /// + [JsonIgnore] + public LoginUserInfo LoginUserInfo { get=> Wtm?.LoginUserInfo;} + + /// + /// 当前Url + /// + [JsonIgnore] + public string CurrentUrl { get => Wtm?.BaseUrl; } + + /// + /// 记录原始提交页面 + /// + [JsonIgnore] + public string FromView { get; set; } + + /// + /// 记录当前页面 + /// + [JsonIgnore] + public string CurrentView { get; set; } + + /// + /// Session信息 + /// + [JsonIgnore] + public ISessionService Session { get => Wtm?.Session; } + + /// + /// Controller传递过来的ModelState信息 + /// + [JsonIgnore] + public IModelStateService MSD { get => Wtm?.MSD; } + + /// + /// 用于保存删除的附件ID + /// + public List DeletedFileIds { get; set; } = new List(); + + [JsonIgnore] + public string ControllerName { get; set; } + + [JsonIgnore] + public IStringLocalizer Localizer { get => Wtm?.Localizer; } + #endregion + + #region Event + + /// + /// InitVM完成后触发的事件 + /// + public event Action OnAfterInit; + /// + /// ReInitVM完成后触发的事件 + /// + public event Action OnAfterReInit; + + #endregion + + #region Method + + /// + /// 调用 InitVM 并触发 OnAfterInit 事件 + /// + public void DoInit() + { + InitVM(); + OnAfterInit?.Invoke(this); + } + + /// + /// 调用 ReInitVM 并触发 OnAfterReInit 事件 + /// + public void DoReInit() + { + ReInitVM(); + OnAfterReInit?.Invoke(this); + } + + + + /// + /// 初始化ViewModel,框架会在创建VM实例之后自动调用本函数 + /// + protected virtual void InitVM() + { + } + + /// + /// 从新初始化ViewModel,框架会在验证失败时自动调用本函数 + /// + protected virtual void ReInitVM() + { + InitVM(); + } + + /// + /// 验证函数,MVC会在提交数据的时候自动调用本函数 + /// + /// + public virtual void Validate() + { + return; + } + + /// + /// 将源VM的上数据库上下文,Session,登录用户信息,模型状态信息,缓存信息等内容复制到本VM中 + /// + /// 复制的源 + public void CopyContext(BaseVM vm) + { + Wtm = vm.Wtm; + FC = vm.FC; + CreatorAssembly = vm.CreatorAssembly; + } + + #endregion + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/CS.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/CS.cs new file mode 100644 index 0000000..6c33ecc --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/CS.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using Microsoft.EntityFrameworkCore; + +namespace WalkingTec.Mvvm.Core +{ + public class CS + { + public string Key { get; set; } + public string Value { get; set; } + public DBTypeEnum? DbType { get; set; } + public string Version { get; set; } + public string DbContext { get; set; } + + public ConstructorInfo DcConstructor; + + public IDataContext CreateDC() + { + if (DcConstructor == null) + { + var AllAssembly = Utils.GetAllAssembly(); + List cis = new List(); + if (AllAssembly != null) + { + foreach (var ass in AllAssembly) + { + try + { + var t = ass.GetExportedTypes().Where(x => typeof(DbContext).IsAssignableFrom(x) && x.Name != "DbContext" && x.Name != "FrameworkContext" && x.Name != "EmptyContext").ToList(); + foreach (var st in t) + { + var ci = st.GetConstructor(new Type[] { typeof(CS) }); + if (ci != null) + { + cis.Add(ci); + } + } + } + catch { } + } + string dcname = DbContext; + if (string.IsNullOrEmpty(dcname)) + { + dcname = "DataContext"; + } + DcConstructor = cis.Where(x => x.DeclaringType.Name.ToLower() == dcname.ToLower()).FirstOrDefault(); + if (DcConstructor == null) + { + DcConstructor = cis.FirstOrDefault(); + } + } + } + return (IDataContext)DcConstructor?.Invoke(new object[] { this }); + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/Configs.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/Configs.cs new file mode 100644 index 0000000..81d2349 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/Configs.cs @@ -0,0 +1,480 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Microsoft.Extensions.Logging; +using WalkingTec.Mvvm.Core.ConfigOptions; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// Configs + /// + public class Configs + { + #region ConnectionStrings + + private List _connectStrings; + + /// + /// ConnectionStrings + /// + public List Connections + { + get + { + if (_connectStrings == null) + { + _connectStrings = new List(); + } + return _connectStrings; + } + set + { + _connectStrings = value; + } + } + + #endregion + + #region Domains + + private Dictionary _domains; + + /// + /// ConnectionStrings + /// + public Dictionary Domains + { + get + { + if (_domains == null) + { + _domains = new Dictionary(); + } + return _domains; + } + set + { + _domains = value; + foreach (var item in _domains) + { + if(item.Value != null) + { + item.Value.Name = item.Key; + } + } + } + } + + #endregion + + + #region QuickDebug + + private bool? _isQuickDebug; + + /// + /// Is debug mode + /// + public bool IsQuickDebug + { + get + { + return _isQuickDebug ?? false; + } + set + { + _isQuickDebug = value; + } + } + + #endregion + + public string ErrorHandler { get; set; } = "/_Framework/Error"; + + #region Cookie prefix + + private string _cookiePre; + + /// + /// Cookie prefix + /// + public string CookiePre + { + get + { + return _cookiePre ?? string.Empty; + } + set + { + _cookiePre = value; + } + } + + #endregion + + #region PageMode + + private PageModeEnum? _pageMode; + + /// + /// PageMode + /// + public PageModeEnum PageMode + { + get + { + if (_pageMode == null) + { + _pageMode = PageModeEnum.Single; + } + return _pageMode.Value; + } + set + { + _pageMode = value; + } + } + #endregion + + #region TabMode + + private TabModeEnum? _tabMode; + + /// + /// TabMode + /// + public TabModeEnum TabMode + { + get + { + if (_tabMode == null) + { + _tabMode = TabModeEnum.Default; + } + return _tabMode.Value; + } + set + { + _tabMode = value; + } + } + #endregion + + #region BlazorMode + + private BlazorModeEnum? _blazorMode; + + /// + /// TabMode + /// + public BlazorModeEnum BlazorMode + { + get + { + if (_blazorMode == null) + { + _blazorMode = BlazorModeEnum.Server; + } + return _blazorMode.Value; + } + set + { + _blazorMode = value; + } + } + #endregion + + + #region Custom settings + + private Dictionary _appSettings; + + /// + /// Custom settings + /// + public Dictionary AppSettings + { + get + { + if (_appSettings == null) + { + _appSettings = new Dictionary(); + } + return _appSettings; + } + set + { + _appSettings = value; + } + } + + #endregion + + #region FileOptions + + private FileUploadOptions _fileUploadOptions; + + /// + /// FileOptions + /// + public FileUploadOptions FileUploadOptions + { + get + { + if (_fileUploadOptions == null) + { + _fileUploadOptions = new FileUploadOptions() + { + UploadLimit = DefaultConfigConsts.DEFAULT_UPLOAD_LIMIT, + SaveFileMode = "database", + Settings = new Dictionary>() + }; + } + return _fileUploadOptions; + } + set + { + _fileUploadOptions = value; + } + } + + #endregion + + #region UIOptions + + private UIOptions _uiOptions; + + /// + /// UIOptions + /// + public UIOptions UIOptions + { + get + { + if (_uiOptions == null) + { + _uiOptions = new UIOptions(); + if (_uiOptions.DataTable == null) + _uiOptions.DataTable = new UIOptions.DataTableOptions + { + RPP = DefaultConfigConsts.DEFAULT_RPP + }; + + if (_uiOptions.ComboBox == null) + _uiOptions.ComboBox = new UIOptions.ComboBoxOptions + { + DefaultEnableSearch = DefaultConfigConsts.DEFAULT_COMBOBOX_DEFAULT_ENABLE_SEARCH + }; + + if (_uiOptions.DateTime == null) + _uiOptions.DateTime = new UIOptions.DateTimeOptions + { + DefaultReadonly = DefaultConfigConsts.DEFAULT_DATETIME_DEFAULT_READONLY + }; + + if (_uiOptions.SearchPanel == null) + _uiOptions.SearchPanel = new UIOptions.SearchPanelOptions + { + DefaultExpand = DefaultConfigConsts.DEFAULT_SEARCHPANEL_DEFAULT_EXPAND + }; + } + return _uiOptions; + } + set + { + _uiOptions = value; + } + } + + #endregion + + #region Is FileAttachment public + + private bool? _isFilePublic; + + /// + /// Is FileAttachment public + /// + public bool IsFilePublic + { + get + { + return _isFilePublic ?? false; + } + set + { + _isFilePublic = value; + } + } + + #endregion + + #region UEditorOptions + + private UEditorOptions _ueditorOptions; + + /// + /// UEditor配置 + /// + /// + public UEditorOptions UEditorOptions + { + get + { + if (_ueditorOptions == null) + { + _ueditorOptions = new UEditorOptions(); + } + return _ueditorOptions; + } + set + { + _ueditorOptions = value; + } + } + #endregion + + #region Cors configs + + private Cors _cors; + + /// + /// Cors configs + /// + public Cors CorsOptions + { + get + { + if (_cors == null) + { + _cors = new Cors(); + _cors.Policy = new List(); + } + return _cors; + } + set + { + _cors = value; + } + } + + #endregion + + #region Support Languages + + private string _languages; + + /// + /// Support Languages + /// + public string Languages + { + get + { + if (string.IsNullOrEmpty((_languages))) + { + _languages = "zh"; + } + return _languages; + } + set + { + _languages = value; + } + } + + private List _supportLanguages; + public List SupportLanguages + { + get + { + if(_supportLanguages == null) + { + _supportLanguages = new List(); + var lans = Languages.Split(","); + foreach (var lan in lans) + { + _supportLanguages.Add(new CultureInfo(lan)); + } + + } + return _supportLanguages; + } + } + + #endregion + + public string HostRoot { get; set; } = ""; + + + #region CookieOption configs + + private CookieOption _cookieOption; + + /// + /// Cors configs + /// + public CookieOption CookieOptions + { + get + { + if (_cookieOption == null) + { + _cookieOption = new CookieOption(); + } + return _cookieOption; + } + set + { + _cookieOption = value; + } + } + + #endregion + + #region JwtOption configs + + private JwtOption _jwtOption; + + /// + /// Cors configs + /// + public JwtOption JwtOptions + { + get + { + if (_jwtOption == null) + { + _jwtOption = new JwtOption(); + } + return _jwtOption; + } + set + { + _jwtOption = value; + if(_jwtOption.SecurityKey.Length < 18) + { + var count = 18 - _jwtOption.SecurityKey.Length; + for (int i = 0; i < count; i++){ + _jwtOption.SecurityKey += "x"; + } + } + } + } + + #endregion + + public IDataContext CreateDC(string csName = null) + { + if (string.IsNullOrEmpty(csName)) + { + csName = "default"; + } + var cs = Connections.Where(x => x.Key.ToLower() == csName.ToLower()).FirstOrDefault(); + return cs?.CreateDC(); + } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/CookieOptions.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/CookieOptions.cs new file mode 100644 index 0000000..d0db150 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/CookieOptions.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Authentication.Cookies; + +namespace WalkingTec.Mvvm.Core +{ + public class CookieOption + { + public string Issuer { get; set; } = "http://localhost"; + public string Audience { get; set; } = "http://localhost"; + public int Expires { get; set; } = 3600; + public bool SlidingExpiration { get; set; } = true; + public string LoginPath { get; set; } = "/Login/Login"; + public string LogoutPath { get; set; } = "/Login/Logout"; + public string AccessDeniedPath { get; set; } = "/Login/Login"; + public string Domain { get; set; } = ""; + public string ReturnUrlParameter { get; set; } = CookieAuthenticationDefaults.ReturnUrlParameter; + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/Cors.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/Cors.cs new file mode 100644 index 0000000..c8b2c18 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/Cors.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace WalkingTec.Mvvm.Core +{ + public class Cors + { + public bool EnableAll { get; set; } + public List Policy { get; set; } + } + + public class CorsPolicy + { + public string Name { get; set; } + public string Domain { get; set; } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/DFS.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/DFS.cs new file mode 100644 index 0000000..c48c11d --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/DFS.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// DFS + /// + public class DFS + { + /// + /// StorageMaxConnection + /// + public int? StorageMaxConnection { get; set; } + + /// + /// TrackerMaxConnection + /// + public int? TrackerMaxConnection { get; set; } + + /// + /// ConnectionTimeout + /// + public int? ConnectionTimeout { get; set; } + + /// + /// ConnectionLifeTime + /// + public int? ConnectionLifeTime { get; set; } + + /// + /// Trackers + /// + public List Trackers { get; set; } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/DFSTracker.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/DFSTracker.cs new file mode 100644 index 0000000..b066df2 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/DFSTracker.cs @@ -0,0 +1,18 @@ +namespace WalkingTec.Mvvm.Core +{ + /// + /// DFSTracker + /// + public class DFSTracker + { + /// + /// IP + /// + public string IP { get; set; } + + /// + /// Port + /// + public int Port { get; set; } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/DefaultConfigConsts.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/DefaultConfigConsts.cs new file mode 100644 index 0000000..4846060 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/DefaultConfigConsts.cs @@ -0,0 +1,38 @@ +namespace WalkingTec.Mvvm.Core.ConfigOptions +{ + /// + /// DefaultConfigConsts + /// + public static class DefaultConfigConsts + { + /// + /// 默认上传路径 + /// + public const string DEFAULT_UPLOAD_DIR = ".\\upload"; + + /// + /// 默认列表行数 + /// + public const int DEFAULT_RPP = 20; + + /// + /// 默认上传文件限制 + /// + public const int DEFAULT_UPLOAD_LIMIT = 20 * 1024 * 1024; + + /// + /// 默认允许ComboBox搜索 + /// + public const bool DEFAULT_COMBOBOX_DEFAULT_ENABLE_SEARCH = true; + + /// + /// 默认开启DateTime只读 + /// + public const bool DEFAULT_DATETIME_DEFAULT_READONLY = true; + + /// + /// 默认展开SearchPanel内容 + /// + public const bool DEFAULT_SEARCHPANEL_DEFAULT_EXPAND = true; + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/Domain.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/Domain.cs new file mode 100644 index 0000000..0136a87 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/Domain.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WalkingTec.Mvvm.Core.ConfigOptions +{ + public class Domain + { + public string Name { get; set; } + + public string Address { get; set; } + + public string InnerAddress { get; set; } + + public string EntryUrl { get; set; } + + public string Url + { + get + { + var rv = Address; + if (string.IsNullOrEmpty(rv) == false && rv.ToLower().StartsWith("http://") == false && rv.ToLower().StartsWith("https://") == false) + { + rv = "http://" + rv; + } + return rv; + } + } + + public string InnerUrl + { + get + { + var rv = InnerAddress; + if (string.IsNullOrEmpty(rv) == false && rv.ToLower().StartsWith("http://") == false && rv.ToLower().StartsWith("https://") == false) + { + rv = "http://" + rv; + } + return rv; + } + } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/FileUploadOptions.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/FileUploadOptions.cs new file mode 100644 index 0000000..552859a --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/FileUploadOptions.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace WalkingTec.Mvvm.Core.ConfigOptions +{ + /// + /// FileOptions + /// + public class FileUploadOptions + { + /// + /// 文件保存位置 + /// + public string SaveFileMode { get; set; } + + /// + /// 上传文件限制 单位字节 默认 20 * 1024 * 1024 = 20971520 bytes + /// + public long UploadLimit { get; set; } = 20971520; + + + public Dictionary> Settings { get; set; } + + } + + public class FileHandlerOptions + { + public string GroupName { get; set; } + public string GroupLocation { get; set; } + public string ServerUrl { get; set; } + + public string Key { get; set; } + public string Secret { get; set; } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/JwtOptions.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/JwtOptions.cs new file mode 100644 index 0000000..2fc8cd7 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/JwtOptions.cs @@ -0,0 +1,14 @@ +using System.Text; + +namespace WalkingTec.Mvvm.Core +{ + public class JwtOption + { + public string Issuer { get; set; } = "http://localhost"; + public string Audience { get; set; } = "http://localhost"; + public int Expires { get; set; } = 3600; + public string SecurityKey { get; set; } = "wtm"; + public string LoginPath { get; set; } + public int RefreshTokenExpires { get; set; } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/KV.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/KV.cs new file mode 100644 index 0000000..a62d27c --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/KV.cs @@ -0,0 +1,19 @@ +namespace WalkingTec.Mvvm.Core +{ + /// + /// KV + /// + public class KV + { + /// + /// Key + /// + public string Key { get; set; } + + /// + /// Value + /// + public string Value { get; set; } + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/UEditorOptions.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/UEditorOptions.cs new file mode 100644 index 0000000..230815b --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/UEditorOptions.cs @@ -0,0 +1,386 @@ + +using System.Text.Json.Serialization; + +namespace WalkingTec.Mvvm.Core.ConfigOptions +{ + public class UEditorOptions + { + #region 上传图片配置项 + /// + /// 执行上传图片的action名称 + /// + /// + [JsonPropertyName("imageActionName")] + public string ImageActionName { get; set; } = "UploadForLayUIUEditor"; + + /// + /// 提交的图片表单名称 + /// + /// + [JsonPropertyName("imageFieldName")] + public string ImageFieldName { get; set; } = "FileID"; + + /// + /// 上传大小限制,单位B + /// + /// + [JsonPropertyName("imageMaxSize")] + public int ImageMaxSize { get; set; } = 2048000; + + /// + /// 上传图片格式显示 + /// + /// + [JsonPropertyName("imageAllowFiles")] + public string[] ImageAllowFiles { get; set; } = new string[] { ".png", ".jpg", ".jpeg", ".gif", ".bmp" }; + + /// + /// 是否压缩图片,默认是true + /// + /// + [JsonPropertyName("imageCompressEnable")] + public bool ImageCompressEnable { get; set; } = true; + + /// + /// 图片压缩最长边限制 + /// + /// + [JsonPropertyName("imageCompressBorder")] + public int ImageCompressBorder { get; set; } = 1600; + + /// + /// 插入的图片浮动方式 + /// + /// + [JsonPropertyName("imageInsertAlign")] + public string ImageInsertAlign { get; set; } = "none"; + + /// + /// 图片访问路径前缀 默认返回全路径 + /// + /// + [JsonPropertyName("imageUrlPrefix")] + public string ImageUrlPrefix { get; set; } = string.Empty; + + /* {filename} 会替换成原文件名,配置这项需要注意中文乱码问题 */ + /* {rand:6} 会替换成随机数,后面的数字是随机数的位数 */ + /* {time} 会替换成时间戳 */ + /* {yyyy} 会替换成四位年份 */ + /* {yy} 会替换成两位年份 */ + /* {mm} 会替换成两位月份 */ + /* {dd} 会替换成两位日期 */ + /* {hh} 会替换成两位小时 */ + /* {ii} 会替换成两位分钟 */ + /* {ss} 会替换成两位秒 */ + /* 非法字符 \ : * ? " < > | */ + /* 具请体看线上文档: fex.baidu.com/ueditor/#use-format_upload_filename */ + + /// + /// 上传保存路径,可以自定义保存路径和文件名格式 + /// + /// + [JsonPropertyName("imagePathFormat")] + public string ImagePathFormat { get; set; } = "upload/image/{yyyy}{mm}{dd}/{time}{rand:6}"; + + #endregion + + #region 涂鸦图片上传配置项 + + /// + /// 执行上传涂鸦的action名称 + /// + /// + [JsonPropertyName("scrawlActionName")] + public string ScrawlActionName { get; set; } = "UploadForLayUIUEditor"; + + /// + /// 提交的图片表单名称 + /// + /// + [JsonPropertyName("scrawlFieldName")] + public string ScrawlFieldName { get; set; } = "FileID"; + + /// + /// 上传保存路径,可以自定义保存路径和文件名格式 + /// + /// + [JsonPropertyName("scrawlPathFormat")] + public string ScrawlPathFormat { get; set; } = "upload/image/{yyyy}{mm}{dd}/{time}{rand:6}"; + + /// + /// 上传大小限制,单位B + /// + /// + [JsonPropertyName("scrawlMaxSize")] + public int ScrawlMaxSize { get; set; } = 2048000; + + /// + /// 图片访问路径前缀 + /// + /// + [JsonPropertyName("scrawlUrlPrefix")] + public string ScrawlUrlPrefix { get; set; } = string.Empty; + + /// + /// 插入的图片浮动方式 + /// + /// + [JsonPropertyName("scrawlInsertAlign")] + public string ScrawlInsertAlign { get; set; } = "none"; + #endregion + + #region 截图工具上传 + + /// + /// 执行上传截图的action名称 + /// + /// + [JsonPropertyName("snapscreenActionName")] + public string SnapscreenActionName { get; set; } = "UploadForLayUIUEditor"; + + /// + /// 上传保存路径,可以自定义保存路径和文件名格式 + /// + /// + [JsonPropertyName("snapscreenPathFormat")] + public string SnapscreenPathFormat { get; set; } = "upload/image/{yyyy}{mm}{dd}/{time}{rand:6}"; + + /// + /// 图片访问路径前缀 + /// + /// + [JsonPropertyName("snapscreenUrlPrefix")] + public string SnapscreenUrlPrefix { get; set; } = string.Empty; + + /// + /// 插入的图片浮动方式 + /// + /// + [JsonPropertyName("snapscreenInsertAlign")] + public string SnapscreenInsertAlign { get; set; } = "none"; + #endregion + + #region 抓取远程图片配置 + + [JsonPropertyName("catcherLocalDomain")] + public string[] CatcherLocalDomain { get; set; } = new string[] { "127.0.0.1", "localhost", "img.baidu.com" }; + + /// + /// 执行抓取远程图片的action名称 + /// + /// + [JsonPropertyName("catcherActionName")] + public string CatcherActionName { get; set; } = "catchimage"; + + /// + /// 提交的图片列表表单名称 + /// + /// + [JsonPropertyName("catcherFieldName")] + public string CatcherFieldName { get; set; } = "source"; + + /// + /// 上传保存路径,可以自定义保存路径和文件名格式 + /// + /// + [JsonPropertyName("catcherPathFormat")] + public string CatcherPathFormat { get; set; } = "upload/image/{yyyy}{mm}{dd}/{time}{rand:6}"; + + /// + /// 图片访问路径前缀 + /// + /// + [JsonPropertyName("catcherUrlPrefix")] + public string CatcherUrlPrefix { get; set; } = string.Empty; + + /// + /// 上传大小限制,单位B + /// + /// + [JsonPropertyName("catcherMaxSize")] + public int CatcherMaxSize { get; set; } = 2048000; + + /// + /// 抓取图片格式显示 + /// + /// + [JsonPropertyName("catcherAllowFiles")] + public string[] CatcherAllowFiles { get; set; } = new string[] { ".png", ".jpg", ".jpeg", ".gif", ".bmp" }; + #endregion + + #region 上传视频配置 + + /// + /// 执行上传视频的action名称 + /// + /// + [JsonPropertyName("videoActionName")] + public string VideoActionName { get; set; } = "UploadForLayUIUEditor"; + + /// + /// 提交的视频表单名称 + /// + /// + [JsonPropertyName("videoFieldName")] + public string VideoFieldName { get; set; } = "FileID"; + + /// + /// 上传保存路径,可以自定义保存路径和文件名格式 + /// + /// + [JsonPropertyName("videoPathFormat")] + public string VideoPathFormat { get; set; } = "upload/video/{yyyy}{mm}{dd}/{time}{rand:6}"; + + /// + /// 视频访问路径前缀 + /// + /// + [JsonPropertyName("videoUrlPrefix")] + public string VideoUrlPrefix { get; set; } = string.Empty; + + /// + /// 上传大小限制,单位B,默认100MB + /// + /// + [JsonPropertyName("videoMaxSize")] + public int VideoMaxSize { get; set; } = 102400000; + + /// + /// 上传视频格式显示 + /// + /// + [JsonPropertyName("videoAllowFiles")] + public string[] VideoAllowFiles { get; set; } = new string[] { ".flv", ".swf", ".mkv", ".avi", ".rm", ".rmvb", ".mpeg", ".mpg", ".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".mid" }; + + #endregion + + #region 上传文件配置 + + /// + /// controller里,执行上传视频的action名称 + /// + /// + [JsonPropertyName("fileActionName")] + public string FileActionName { get; set; } = "UploadForLayUIUEditor"; + + /// + /// 提交的文件表单名称 + /// + /// + [JsonPropertyName("fileFieldName")] + public string FileFieldName { get; set; } = "FileID"; + + /// + /// 上传保存路径,可以自定义保存路径和文件名格式 + /// + /// + [JsonPropertyName("filePathFormat")] + public string FilePathFormat { get; set; } = "upload/file/{yyyy}{mm}{dd}/{time}{rand:6}"; + + /// + /// 文件访问路径前缀 + /// + /// + [JsonPropertyName("fileUrlPrefix")] + public string FileUrlPrefix { get; set; } = string.Empty; + + /// + /// 上传大小限制,单位B,默认50MB + /// + /// + [JsonPropertyName("fileMaxSize")] + public int FileMaxSize { get; set; } = 51200000; + + /// + /// 上传文件格式显示 + /// + /// + [JsonPropertyName("fileAllowFiles")] + public string[] FileAllowFiles { get; set; } = new string[] { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".flv", ".swf", ".mkv", ".avi", ".rm", ".rmvb", ".mpeg", ".mpg", ".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".mid", ".rar", ".zip", ".tar", ".gz", ".7z", ".bz2", ".cab", ".iso", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".pdf", ".txt", ".md", ".xml" }; + + #endregion + + #region 列出指定目录下的图片 + + /// + /// 执行图片管理的action名称 + /// + /// + [JsonPropertyName("imageManagerActionName")] + public string ImageManagerActionName { get; set; } = "listimage"; + + /// + /// 指定要列出图片的目录 + /// + /// + [JsonPropertyName("imageManagerListPath")] + public string ImageManagerListPath { get; set; } = string.Empty; + + /// + /// 每次列出文件数量 + /// + /// + [JsonPropertyName("imageManagerListSize")] + public int ImageManagerListSize { get; set; } = 20; + + /// + /// 图片访问路径前缀 + /// + /// + [JsonPropertyName("imageManagerUrlPrefix")] + public string ImageManagerUrlPrefix { get; set; } = string.Empty; + + /// + /// 插入的图片浮动方式 + /// + /// + [JsonPropertyName("imageManagerInsertAlign")] + public string ImageManagerInsertAlign { get; set; } = "none"; + + /// + /// 列出的文件类型 + /// + /// + [JsonPropertyName("imageManagerAllowFiles")] + public string[] ImageManagerAllowFiles { get; set; } = new string[] { ".png", ".jpg", ".jpeg", ".gif", ".bmp" }; + #endregion + + #region 列出指定目录下的文件 + + /// + /// 执行文件管理的action名称 + /// + /// + [JsonPropertyName("fileManagerActionName")] + public string FileManagerActionName { get; set; } = "listfile"; + + /// + /// 指定要列出文件的目录 + /// + /// + [JsonPropertyName("fileManagerListPath")] + public string FileManagerListPath { get; set; } = "upload/file"; + + /// + /// 文件访问路径前缀 + /// + /// + [JsonPropertyName("fileManagerUrlPrefix")] + public string FileManagerUrlPrefix { get; set; } = "/ueditor/net/"; + + /// + /// 每次列出文件数量 + /// + /// + [JsonPropertyName("fileManagerListSize")] + public int FileManagerListSize { get; set; } = 20; + + /// + /// 列出的文件类型 + /// + /// + [JsonPropertyName("fileManagerAllowFiles")] + public string[] FileManagerAllowFiles { get; set; } = new string[] { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".flv", ".swf", ".mkv", ".avi", ".rm", ".rmvb", ".mpeg", ".mpg", ".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".mid", ".rar", ".zip", ".tar", ".gz", ".7z", ".bz2", ".cab", ".iso", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".pdf", ".txt", ".md", ".xml" }; + #endregion + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/UIOptions.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/UIOptions.cs new file mode 100644 index 0000000..7766e5a --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ConfigOptions/UIOptions.cs @@ -0,0 +1,49 @@ +namespace WalkingTec.Mvvm.Core.ConfigOptions +{ + public class UIOptions + { + public DataTableOptions DataTable { get; set; } + public ComboBoxOptions ComboBox { get; set; } + public DateTimeOptions DateTime { get; set; } + public SearchPanelOptions SearchPanel { get; set; } + + public class DataTableOptions + { + /// + /// 默认列表行数 + /// + public int RPP { get; set; } + + public bool ShowPrint { get; set; } + + public bool ShowFilter { get; set; } + } + + public class ComboBoxOptions + { + + /// + /// 默认允许ComboBox搜索 + /// + public bool DefaultEnableSearch { get; set; } + } + + public class DateTimeOptions + { + + /// + /// 默认开启DateTime只读 + /// + public bool DefaultReadonly { get; set; } + } + + public class SearchPanelOptions + { + + /// + /// 默认展开SearchPanel内容 + /// + public bool DefaultExpand { get; set; } + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/CoreProgram.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/CoreProgram.cs new file mode 100644 index 0000000..e46bb30 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/CoreProgram.cs @@ -0,0 +1,34 @@ +using System.Text.Json; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; + +namespace WalkingTec.Mvvm.Core +{ + public class CoreProgram + { + public static IStringLocalizer _localizer { + get; + set; + } + + public static JsonSerializerOptions DefaultJsonOption + { + get;set; + } + + public static JsonSerializerOptions DefaultPostJsonOption + { + get; set; + } + + + public static string[] Buildindll = new string[] + { + "WalkingTec.Mvvm.Core", + "WalkingTec.Mvvm.Mvc", + "WalkingTec.Mvvm.Admin", + "WalkingTec.Mvvm.Taghelpers" + }; + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/DataContext.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/DataContext.cs new file mode 100644 index 0000000..3d97f2e --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/DataContext.cs @@ -0,0 +1,1269 @@ +using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Console; +using Microsoft.Extensions.Logging.Debug; +using Microsoft.Extensions.Options; +using MySqlConnector; +using Npgsql; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using WalkingTec.Mvvm.Core.Extensions; +using WalkingTec.Mvvm.Core.Support.Json; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// FrameworkContext + /// + public partial class FrameworkContext : EmptyContext, IDataContext + { + public DbSet BaseFrameworkMenus { get; set; } + public DbSet BaseFunctionPrivileges { get; set; } + public DbSet BaseDataPrivileges { get; set; } + public DbSet BaseFileAttachments { get; set; } + public DbSet BaseFrameworkRoles { get; set; } + public DbSet BaseFrameworkUserRoles { get; set; } + public DbSet BaseFrameworkUserGroups { get; set; } + public DbSet BaseFrameworkGroups { get; set; } + public DbSet BaseActionLogs { get; set; } + //public DbSet BaseFrameworkAreas { get; set; } + public DbSet PersistedGrants { get; set; } + + /// + /// FrameworkContext + /// + public FrameworkContext() : base() + { + } + + /// + /// FrameworkContext + /// + /// + public FrameworkContext(string cs) : base(cs) + { + } + + public FrameworkContext(string cs, DBTypeEnum dbtype, string version = null) : base(cs, dbtype, version) + { + } + + public FrameworkContext(CS cs) : base(cs) + { + } + public FrameworkContext(DbContextOptions options) : base(options) { } + + /// + /// OnModelCreating + /// + /// + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + //菜单和菜单权限的级联删除 + modelBuilder.Entity().HasOne(x => x.MenuItem).WithMany(x => x.Privileges).HasForeignKey(x => x.MenuItemId).OnDelete(DeleteBehavior.Cascade); + + var modelAsms = Utils.GetAllAssembly(); + + var allTypes = new List();// 所有 DbSet<> 的泛型类型 + + #region 获取所有 DbSet 的泛型类型 T 及其 List 类型属性对应的类型 T + + // 获取所有 DbSet 的泛型类型 T + foreach (var asm in modelAsms) + { + try + { + var dcModule = asm.GetExportedTypes().Where(x => typeof(DbContext).IsAssignableFrom(x)).ToList(); + if (dcModule != null && dcModule.Count > 0) + { + foreach (var module in dcModule) + { + foreach (var pro in module.GetProperties()) + { + if (pro.PropertyType.IsGeneric(typeof(DbSet<>))) + { + if (!allTypes.Contains(pro.PropertyType.GenericTypeArguments[0], new TypeComparer())) + { + allTypes.Add(pro.PropertyType.GenericTypeArguments[0]); + } + } + } + } + } + } + catch { } + } + + // 获取类型 T 下 List 类型的属性对应的类型 S,且S 必须是 TopBasePoco 的子类,只有这些类会生成库 + for (int i = 0; i < allTypes.Count; i++) // + { + var item = allTypes[i]; + var pros = item.GetProperties(); + foreach (var pro in pros) + { + if (typeof(TopBasePoco).IsAssignableFrom(pro.PropertyType)) + { + if (allTypes.Contains(pro.PropertyType) == false) + { + allTypes.Add(pro.PropertyType); + } + } + else + { + if (pro.PropertyType.IsGenericType && pro.PropertyType.GetGenericTypeDefinition() == typeof(List<>)) + { + var inner = pro.PropertyType.GetGenericArguments()[0]; + if (typeof(TopBasePoco).IsAssignableFrom(inner)) + { + if (allTypes.Contains(inner) == false) + { + allTypes.Add(inner); + } + } + } + } + } + } + + #endregion + foreach (var item in allTypes) + { + if (typeof(TopBasePoco).IsAssignableFrom(item) && typeof(ISubFile).IsAssignableFrom(item) == false) + { + //将所有关联附件的外键设为不可级联删除 + var pros = item.GetProperties().Where(x => x.PropertyType == typeof(FileAttachment)).ToList(); + foreach (var filepro in pros) + { + var builder = typeof(ModelBuilder).GetMethod("Entity", Type.EmptyTypes).MakeGenericMethod(item).Invoke(modelBuilder, null) as EntityTypeBuilder; + builder.HasOne(filepro.Name).WithMany().OnDelete(DeleteBehavior.Restrict); + } + } + } + } + + + /// + /// 数据初始化 + /// + /// + /// + /// 返回true表示需要进行初始化数据操作,返回false即数据库已经存在或不需要初始化数据 + public async override Task DataInit(object allModules, bool IsSpa) + { + bool rv = await Database.EnsureCreatedAsync(); + //判断是否存在初始数据 + bool emptydb = false; + try + { + emptydb = Set().Count() == 0 && Set().Count() == 0; + } + catch { } + + if (emptydb == true) + { + var AllModules = allModules as List; + var roles = new FrameworkRole[] + { + new FrameworkRole{ ID = Guid.NewGuid(), RoleCode = "001", RoleName = CoreProgram._localizer?["Sys.Admin"]} + }; + + var adminRole = roles[0]; + if (Set().Any() == false) + { + var systemManagement = GetFolderMenu("SystemManagement", new List { adminRole }); + var logList = IsSpa ? GetMenu2(AllModules, "ActionLog", "MenuKey.ActionLog", new List { adminRole }, 1) : GetMenu(AllModules, "_Admin", "ActionLog", "Index", "MenuKey.ActionLog", new List { adminRole }, 1); + var userList = IsSpa ? GetMenu2(AllModules, "FrameworkUser", "MenuKey.UserManagement", new List { adminRole }, 2) : GetMenu(AllModules, "_Admin", "FrameworkUser", "Index", "MenuKey.UserManagement", new List { adminRole }, 2); + var roleList = IsSpa ? GetMenu2(AllModules, "FrameworkRole", "MenuKey.RoleManagement", new List { adminRole }, 3) : GetMenu(AllModules, "_Admin", "FrameworkRole", "Index", "MenuKey.RoleManagement", new List { adminRole }, 3); + var groupList = IsSpa ? GetMenu2(AllModules, "FrameworkGroup", "MenuKey.GroupManagement", new List { adminRole }, 4) : GetMenu(AllModules, "_Admin", "FrameworkGroup", "Index", "MenuKey.GroupManagement", new List { adminRole }, 4); + var menuList = IsSpa ? GetMenu2(AllModules, "FrameworkMenu", "MenuKey.MenuMangement", new List { adminRole }, 5) : GetMenu(AllModules, "_Admin", "FrameworkMenu", "Index", "MenuKey.MenuMangement", new List { adminRole }, 5); + var dpList = IsSpa ? GetMenu2(AllModules, "DataPrivilege", "MenuKey.DataPrivilege", new List { adminRole }, 6) : GetMenu(AllModules, "_Admin", "DataPrivilege", "Index", "MenuKey.DataPrivilege", new List { adminRole }, 6); + if (logList != null) + { + var menus = new FrameworkMenu[] { logList, userList, roleList, groupList, menuList, dpList }; + foreach (var item in menus) + { + if (item != null) + { + systemManagement.Children.Add(item); + } + } + Set().Add(systemManagement); + + if (IsSpa == false) + { + systemManagement.Icon = "layui-icon layui-icon-set"; + logList?.SetPropertyValue("Icon", "layui-icon layui-icon-form"); + userList?.SetPropertyValue("Icon", "layui-icon layui-icon-friends"); + roleList?.SetPropertyValue("Icon", "layui-icon layui-icon-user"); + groupList?.SetPropertyValue("Icon", "layui-icon layui-icon-group"); + menuList?.SetPropertyValue("Icon", "layui-icon layui-icon-menu-fill"); + dpList?.SetPropertyValue("Icon", "layui-icon layui-icon-auz"); + + var apifolder = GetFolderMenu("Api", new List { adminRole }); + apifolder.ShowOnMenu = false; + apifolder.DisplayOrder = 100; + var logList2 = GetMenu2(AllModules, "ActionLog", "MenuKey.ActionLog", new List { adminRole }, 1); + var userList2 = GetMenu2(AllModules, "FrameworkUser", "MenuKey.UserManagement", new List { adminRole }, 2); + var roleList2 = GetMenu2(AllModules, "FrameworkRole", "MenuKey.RoleManagement", new List { adminRole }, 3); + var groupList2 = GetMenu2(AllModules, "FrameworkGroup", "MenuKey.GroupManagement", new List { adminRole }, 4); + var menuList2 = GetMenu2(AllModules, "FrameworkMenu", "MenuKey.MenuMangement", new List { adminRole }, 5); + var dpList2 = GetMenu2(AllModules, "DataPrivilege", "MenuKey.DataPrivilege", new List { adminRole }, 6); + var apis = new FrameworkMenu[] { logList2, userList2, roleList2, groupList2, menuList2, dpList2 }; + //apis.ToList().ForEach(x => { x.ShowOnMenu = false;x.PageName += $"({Program._localizer["BuildinApi"]})"; }); + foreach (var item in apis) + { + if (item != null) + { + apifolder.Children.Add(item); + + } + } + Set().Add(apifolder); + } + else + { + systemManagement.Icon = " _wtmicon _wtmicon-icon_shezhi"; + logList?.SetPropertyValue("Icon", " _wtmicon _wtmicon-chaxun"); + userList?.SetPropertyValue("Icon", " _wtmicon _wtmicon-zhanghaoquanxianguanli"); + roleList?.SetPropertyValue("Icon", " _wtmicon _wtmicon-quanxianshenpi"); + groupList?.SetPropertyValue("Icon", " _wtmicon _wtmicon-zuzhiqunzu"); + menuList?.SetPropertyValue("Icon", " _wtmicon _wtmicon--lumingpai"); + dpList?.SetPropertyValue("Icon", " _wtmicon _wtmicon-anquan"); + } + } + + } + Set().AddRange(roles); + await SaveChangesAsync(); + } + return rv; + } + + private FrameworkMenu GetFolderMenu(string FolderText, List allowedRoles, bool isShowOnMenu = true, bool isInherite = false) + { + FrameworkMenu menu = new FrameworkMenu + { + PageName = "MenuKey." + FolderText, + Children = new List(), + Privileges = new List(), + ShowOnMenu = isShowOnMenu, + IsInside = true, + FolderOnly = true, + IsPublic = false, + DisplayOrder = 1 + }; + + if (allowedRoles != null) + { + foreach (var role in allowedRoles) + { + menu.Privileges.Add(new FunctionPrivilege { RoleCode = role.RoleCode, Allowed = true }); + + } + } + return menu; + } + + private FrameworkMenu GetMenu(List allModules, string areaName, string controllerName, string actionName, string pageKey, List allowedRoles, int displayOrder) + { + var acts = allModules.Where(x => x.ClassName == controllerName && (areaName == null || x.Area?.Prefix?.ToLower() == areaName.ToLower())).SelectMany(x => x.Actions).ToList(); + var act = acts.Where(x => x.MethodName == actionName).SingleOrDefault(); + var rest = acts.Where(x => x.MethodName != actionName && x.IgnorePrivillege == false).ToList(); + FrameworkMenu menu = GetMenuFromAction(act, true, allowedRoles, displayOrder); + if (menu != null) + { + menu.PageName = pageKey; + for (int i = 0; i < rest.Count; i++) + { + if (rest[i] != null) + { + var sub = GetMenuFromAction(rest[i], false, allowedRoles, (i + 1)); + sub.PageName = pageKey; + menu.Children.Add(sub); + } + } + } + return menu; + } + + private FrameworkMenu GetMenu2(List allModules, string controllerName, string pageKey, List allowedRoles, int displayOrder) + { + var acts = allModules.Where(x => (x.FullName == $"WalkingTec.Mvvm.Admin.Api,{controllerName}")&& x.IsApi == true).SelectMany(x => x.Actions).ToList(); + var rest = acts.Where(x => x.IgnorePrivillege == false).ToList(); + SimpleAction act = null; + if (acts.Count > 0) + { + act = acts[0]; + } + FrameworkMenu menu = GetMenuFromAction(act, true, allowedRoles, displayOrder); + if (menu != null) + { + menu.PageName = pageKey; + menu.Url = "/" + acts[0].Module.ClassName.ToLower(); + menu.ActionName = "MainPage"; + menu.ClassName = acts[0].Module.FullName; + menu.MethodName = null; + for (int i = 0; i < rest.Count; i++) + { + if (rest[i] != null) + { + var sub = GetMenuFromAction(rest[i], false, allowedRoles, (i + 1)); + sub.PageName = pageKey; + menu.Children.Add(sub); + } + } + } + return menu; + } + + private FrameworkMenu GetMenuFromAction(SimpleAction act, bool isMainLink, List allowedRoles, int displayOrder = 1) + { + if (act == null) + { + return null; + } + FrameworkMenu menu = new FrameworkMenu + { + //ActionId = act.ID, + //ModuleId = act.ModuleId, + ClassName = act.Module.FullName, + MethodName = act.MethodName, + Url = act.Url, + Privileges = new List(), + ShowOnMenu = isMainLink, + FolderOnly = false, + Children = new List(), + IsPublic = false, + IsInside = true, + DisplayOrder = displayOrder, + }; + if (isMainLink) + { + menu.ModuleName = act.Module.ModuleName; + menu.ActionName = act.ActionDes?.Description ?? act.ActionName; + menu.MethodName = null; + } + else + { + menu.ModuleName = act.Module.ModuleName; + menu.ActionName = act.ActionDes?.Description ?? act.ActionName; + } + if (allowedRoles != null) + { + foreach (var role in allowedRoles) + { + menu.Privileges.Add(new FunctionPrivilege { RoleCode = role.RoleCode, Allowed = true }); + + } + } + return menu; + } + + } + + public partial class EmptyContext : DbContext, IDataContext + { + private ILoggerFactory _loggerFactory; + + /// + /// Commited + /// + public bool Commited { get; set; } + + /// + /// IsFake + /// + public bool IsFake { get; set; } + + public bool IsDebug { get; set; } + /// + /// CSName + /// + public string CSName { get; set; } + + public DBTypeEnum DBType { get; set; } + + public string Version { get; set; } + public CS ConnectionString { get; set; } + /// + /// FrameworkContext + /// + public EmptyContext() + { + CSName = "default"; + DBType = DBTypeEnum.SqlServer; + } + + /// + /// FrameworkContext + /// + /// + public EmptyContext(string cs) + { + CSName = cs; + DBType = DBTypeEnum.SqlServer; + } + + public EmptyContext(string cs, DBTypeEnum dbtype, string version = null) + { + CSName = cs; + DBType = dbtype; + Version = version; + } + + public EmptyContext(CS cs) + { + CSName = cs.Value; + DBType = cs.DbType ?? DBTypeEnum.SqlServer; + Version = cs.Version; + ConnectionString = cs; + } + + public EmptyContext(DbContextOptions options) : base(options) { } + + public IDataContext CreateNew() + { + if (ConnectionString != null) + { + return (IDataContext)this.GetType().GetConstructor(new Type[] { typeof(CS) }).Invoke(new object[] { ConnectionString }); ; + } + else + { + return (IDataContext)this.GetType().GetConstructor(new Type[] { typeof(string), typeof(DBTypeEnum), typeof(string) }).Invoke(new object[] { CSName, DBType, Version }); + } + } + + public IDataContext ReCreate() + { + if (this?.Database?.CurrentTransaction != null) + { + return this; + } + else + { + if (ConnectionString != null) + { + return (IDataContext)this.GetType().GetConstructor(new Type[] { typeof(CS) }).Invoke(new object[] { ConnectionString }); ; + } + else + { + return (IDataContext)this.GetType().GetConstructor(new Type[] { typeof(string), typeof(DBTypeEnum) }).Invoke(new object[] { CSName, DBType }); + } + } + } + /// + /// 将一个实体设为填加状态 + /// + /// 实体 + public void AddEntity(T entity) where T : TopBasePoco + { + this.Entry(entity).State = EntityState.Added; + } + + /// + /// 将一个实体设为修改状态 + /// + /// 实体 + public void UpdateEntity(T entity) where T : TopBasePoco + { + this.Entry(entity).State = EntityState.Modified; + } + + /// + /// 将一个实体的某个字段设为修改状态,用于只更新个别字段的情况 + /// + /// 实体类 + /// 实体 + /// 要设定为修改状态的字段 + public void UpdateProperty(T entity, Expression> fieldExp) + where T : TopBasePoco + { + var set = this.Set(); + if (set.Local.AsQueryable().CheckID(entity.GetID()).FirstOrDefault() == null) + { + set.Attach(entity); + } + this.Entry(entity).Property(fieldExp).IsModified = true; + } + + /// + /// UpdateProperty + /// + /// + /// + /// + public void UpdateProperty(T entity, string fieldName) + where T : TopBasePoco + { + var set = this.Set(); + if (set.Local.AsQueryable().CheckID(entity.GetID()).FirstOrDefault() == null) + { + set.Attach(entity); + } + this.Entry(entity).Property(fieldName).IsModified = true; + } + + /// + /// 将一个实体设定为删除状态 + /// + /// 实体 + public void DeleteEntity(T entity) where T : TopBasePoco + { + var set = this.Set(); + var exist = set.Local.AsQueryable().CheckID(entity.GetID()).FirstOrDefault(); + if (exist == null) + { + set.Attach(entity); + set.Remove(entity); + } + else + { + set.Remove(exist); + + } + } + + /// + /// CascadeDelete + /// + /// + /// + public void CascadeDelete(T entity) where T : TreePoco + { + if (entity != null && entity.ID != Guid.Empty) + { + var set = this.Set(); + var entities = set.Where(x => x.ParentId == entity.ID).ToList(); + if (entities.Count > 0) + { + foreach (var item in entities) + { + CascadeDelete(item); + } + } + DeleteEntity(entity); + } + } + + /// + /// GetCoreType + /// + /// + /// + public Type GetCoreType(Type t) + { + if (t != null && t.IsNullable()) + { + if (!t.GetTypeInfo().IsValueType) + { + return t; + } + else + { + if ("DateTime".Equals(t.GenericTypeArguments[0].Name)) + { + return typeof(string); + } + return Nullable.GetUnderlyingType(t); + } + } + else + { + if ("DateTime".Equals(t.Name)) + { + return typeof(string); + } + return t; + } + } + + /// + /// OnModelCreating + /// + /// + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + if (DBType == DBTypeEnum.Oracle) + { + modelBuilder.Model.SetMaxIdentifierLength(30); + } + } + + /// + /// OnConfiguring + /// + /// + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + switch (DBType) + { + case DBTypeEnum.SqlServer: + optionsBuilder.UseSqlServer(CSName); + break; + case DBTypeEnum.MySql: + ServerVersion sv = null; + if (string.IsNullOrEmpty(Version) == false) + { + ServerVersion.TryParse(Version, out sv); + } + if (sv == null) + { + sv = ServerVersion.AutoDetect(CSName); + } + optionsBuilder.UseMySql(CSName, sv); + break; + case DBTypeEnum.PgSql: + optionsBuilder.UseNpgsql(CSName); + break; + case DBTypeEnum.Memory: + optionsBuilder.UseInMemoryDatabase(CSName); + break; + case DBTypeEnum.SQLite: + optionsBuilder.UseSqlite(CSName); + break; + case DBTypeEnum.Oracle: + + optionsBuilder.UseOracle(CSName, option => + { + if (string.IsNullOrEmpty(Version) == false) + { + option.UseOracleSQLCompatibility(Version); + } + else + { + option.UseOracleSQLCompatibility("11"); + } + }); + break; + default: + break; + } + if (IsDebug == true) + { + optionsBuilder.EnableDetailedErrors(); + optionsBuilder.EnableSensitiveDataLogging(); + if (_loggerFactory != null) + { + optionsBuilder.UseLoggerFactory(_loggerFactory); + } + } + base.OnConfiguring(optionsBuilder); + } + + public void SetLoggerFactory(ILoggerFactory factory) + { + this._loggerFactory = factory; + } + + + /// + /// 数据初始化 + /// + /// + /// + /// 返回true表示需要进行初始化数据操作,返回false即数据库已经存在或不需要初始化数据 + public async virtual Task DataInit(object allModules, bool IsSpa) + { + bool rv = await Database.EnsureCreatedAsync(); + return rv; + } + + #region 执行存储过程返回datatable + /// + /// 执行存储过程,返回datatable结果集 + /// + /// 存储过程名称 + /// 存储过程参数 + /// + public DataTable RunSP(string command, params object[] paras) + { + return Run(command, CommandType.StoredProcedure, paras); + } + #endregion + + public IEnumerable RunSP(string command, params object[] paras) + { + return Run(command, CommandType.StoredProcedure, paras); + } + + #region 执行Sql语句,返回datatable + public DataTable RunSQL(string sql, params object[] paras) + { + return Run(sql, CommandType.Text, paras); + } + #endregion + + public IEnumerable RunSQL(string sql, params object[] paras) + { + return Run(sql, CommandType.Text, paras); + } + + + #region 执行存储过程或Sql语句返回DataTable + /// + /// 执行存储过程或Sql语句返回DataTable + /// + /// 存储过程名称或Sql语句 + /// 命令类型 + /// 参数 + /// + public DataTable Run(string sql, CommandType commandType, params object[] paras) + { + DataTable table = new DataTable(); + var connection = this.Database.GetDbConnection(); + var isClosed = connection.State == ConnectionState.Closed; + if (isClosed) + { + connection.Open(); + } + using (var command = connection.CreateCommand()) + { + command.CommandText = sql; + command.CommandTimeout = 2400; + command.CommandType = commandType; + if (this.Database.CurrentTransaction != null) + { + command.Transaction = this.Database.CurrentTransaction.GetDbTransaction(); + } + if (paras != null) + { + foreach (var param in paras) + command.Parameters.Add(param); + } + using (var reader = command.ExecuteReader()) + { + table.Load(reader); + } + } + if (isClosed) + { + connection.Close(); + } + return table; + } + #endregion + + + public IEnumerable Run(string sql, CommandType commandType, params object[] paras) + { + IEnumerable entityList = new List(); + DataTable dt = Run(sql, commandType, paras); + entityList = EntityHelper.GetEntityList(dt); + return entityList; + } + + + public object CreateCommandParameter(string name, object value, ParameterDirection dir) + { + object rv = null; + switch (this.DBType) + { + case DBTypeEnum.SqlServer: + rv = new SqlParameter(name, value) { Direction = dir }; + break; + case DBTypeEnum.MySql: + rv = new MySqlParameter(name, value) { Direction = dir }; + break; + case DBTypeEnum.PgSql: + rv = new NpgsqlParameter(name, value) { Direction = dir }; + break; + case DBTypeEnum.SQLite: + rv = new SqliteParameter(name, value) { Direction = dir }; + break; + case DBTypeEnum.Oracle: + //rv = new OracleParameter(name, value) { Direction = dir }; + break; + } + return rv; + } + } + + public class NullContext : IDataContext + { + + + public bool IsFake { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public IModel Model => throw new NotImplementedException(); + + public DatabaseFacade Database => throw new NotImplementedException(); + + public string CSName { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public DBTypeEnum DBType { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public bool IsDebug { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public void AddEntity(T entity) where T : TopBasePoco + { + throw new NotImplementedException(); + } + + public void CascadeDelete(T entity) where T : TreePoco + { + throw new NotImplementedException(); + } + + public object CreateCommandParameter(string name, object value, ParameterDirection dir) + { + throw new NotImplementedException(); + } + + public IDataContext CreateNew() + { + throw new NotImplementedException(); + } + + public Task DataInit(object AllModel, bool IsSpa) + { + throw new NotImplementedException(); + } + + public void DeleteEntity(T entity) where T : TopBasePoco + { + throw new NotImplementedException(); + } + + public void Dispose() + { + + } + + public IDataContext ReCreate() + { + throw new NotImplementedException(); + } + + public DataTable Run(string sql, CommandType commandType, params object[] paras) + { + throw new NotImplementedException(); + } + + public IEnumerable Run(string sql, CommandType commandType, params object[] paras) + { + throw new NotImplementedException(); + } + + public DataTable RunSP(string command, params object[] paras) + { + throw new NotImplementedException(); + } + + public IEnumerable RunSP(string command, params object[] paras) + { + throw new NotImplementedException(); + } + + public DataTable RunSQL(string command, params object[] paras) + { + throw new NotImplementedException(); + } + + public IEnumerable RunSQL(string sql, params object[] paras) + { + throw new NotImplementedException(); + } + + public int SaveChanges() + { + throw new NotImplementedException(); + } + + public int SaveChanges(bool acceptAllChangesOnSuccess) + { + throw new NotImplementedException(); + } + + public Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public DbSet Set() where T : class + { + throw new NotImplementedException(); + } + + public void SetLoggerFactory(ILoggerFactory factory) + { + throw new NotImplementedException(); + } + + public void UpdateEntity(T entity) where T : TopBasePoco + { + throw new NotImplementedException(); + } + + public void UpdateProperty(T entity, Expression> fieldExp) where T : TopBasePoco + { + throw new NotImplementedException(); + } + + public void UpdateProperty(T entity, string fieldName) where T : TopBasePoco + { + throw new NotImplementedException(); + } + } + + public partial class PlusContext : EmptyContext, IDataContext + { + public DbSet BaseFrameworkMenus { get; set; } + public DbSet BaseFunctionPrivileges { get; set; } + public DbSet BaseDataPrivileges { get; set; } + public DbSet BaseFileAttachments { get; set; } + public DbSet BaseFrameworkRoles { get; set; } + public DbSet BaseFrameworkGroups { get; set; } + public DbSet BaseActionLogs { get; set; } + //public DbSet BaseFrameworkAreas { get; set; } + public DbSet PersistedGrants { get; set; } + + /// + /// FrameworkContext + /// + public PlusContext() : base() + { + } + + /// + /// FrameworkContext + /// + /// + public PlusContext(string cs) : base(cs) + { + } + + public PlusContext(string cs, DBTypeEnum dbtype, string version = null) : base(cs, dbtype, version) + { + } + + public PlusContext(CS cs) : base(cs) + { + } + public PlusContext(DbContextOptions options) : base(options) { } + + /// + /// OnModelCreating + /// + /// + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + //菜单和菜单权限的级联删除 + modelBuilder.Entity().HasOne(x => x.MenuItem).WithMany(x => x.Privileges).HasForeignKey(x => x.MenuItemId).OnDelete(DeleteBehavior.Cascade); + + var modelAsms = Utils.GetAllAssembly(); + + var allTypes = new List();// 所有 DbSet<> 的泛型类型 + + #region 获取所有 DbSet 的泛型类型 T 及其 List 类型属性对应的类型 T + + // 获取所有 DbSet 的泛型类型 T + foreach (var asm in modelAsms) + { + try + { + var dcModule = asm.GetExportedTypes().Where(x => typeof(DbContext).IsAssignableFrom(x)).ToList(); + if (dcModule != null && dcModule.Count > 0) + { + foreach (var module in dcModule) + { + foreach (var pro in module.GetProperties()) + { + if (pro.PropertyType.IsGeneric(typeof(DbSet<>))) + { + if (!allTypes.Contains(pro.PropertyType.GenericTypeArguments[0], new TypeComparer())) + { + allTypes.Add(pro.PropertyType.GenericTypeArguments[0]); + } + } + } + } + } + } + catch { } + } + + // 获取类型 T 下 List 类型的属性对应的类型 S,且S 必须是 TopBasePoco 的子类,只有这些类会生成库 + for (int i = 0; i < allTypes.Count; i++) // + { + var item = allTypes[i]; + var pros = item.GetProperties(); + foreach (var pro in pros) + { + if (typeof(TopBasePoco).IsAssignableFrom(pro.PropertyType)) + { + if (allTypes.Contains(pro.PropertyType) == false) + { + allTypes.Add(pro.PropertyType); + } + } + else + { + if (pro.PropertyType.IsGenericType && pro.PropertyType.GetGenericTypeDefinition() == typeof(List<>)) + { + var inner = pro.PropertyType.GetGenericArguments()[0]; + if (typeof(TopBasePoco).IsAssignableFrom(inner)) + { + if (allTypes.Contains(inner) == false) + { + allTypes.Add(inner); + } + } + } + } + } + } + + #endregion + foreach (var item in allTypes) + { + if (typeof(TopBasePoco).IsAssignableFrom(item) && typeof(ISubFile).IsAssignableFrom(item) == false) + { + //将所有关联附件的外键设为不可级联删除 + var pros = item.GetProperties().Where(x => x.PropertyType == typeof(FileAttachment)).ToList(); + foreach (var filepro in pros) + { + var builder = typeof(ModelBuilder).GetMethod("Entity", Type.EmptyTypes).MakeGenericMethod(item).Invoke(modelBuilder, null) as EntityTypeBuilder; + builder.HasOne(filepro.Name).WithMany().OnDelete(DeleteBehavior.Restrict); + } + } + } + } + + + /// + /// 数据初始化 + /// + /// + /// + /// 返回true表示需要进行初始化数据操作,返回false即数据库已经存在或不需要初始化数据 + public async override Task DataInit(object allModules, bool IsSpa) + { + bool rv = await Database.EnsureCreatedAsync(); + //判断是否存在初始数据 + bool emptydb = false; + try + { + emptydb = Set().Count() == 0 && Set().Count() == 0; + } + catch { } + + if (emptydb == true) + { + var AllModules = allModules as List; + var roles = new FrameworkRole[] + { + new FrameworkRole{ ID = Guid.NewGuid(), RoleCode = "001", RoleName = CoreProgram._localizer?["Sys.Admin"]} + }; + + var adminRole = roles[0]; + if (Set().Any() == false) + { + var systemManagement = GetFolderMenu("SystemManagement", new List { adminRole }); + var logList = IsSpa ? GetMenu2(AllModules, "ActionLog", "MenuKey.ActionLog", new List { adminRole }, 1) : GetMenu(AllModules, "_Admin", "ActionLog", "Index", "MenuKey.ActionLog", new List { adminRole }, 1); + var userList = IsSpa ? GetMenu2(AllModules, "FrameworkUser", "MenuKey.UserManagement", new List { adminRole }, 2) : GetMenu(AllModules, "_Admin", "FrameworkUser", "Index", "MenuKey.UserManagement", new List { adminRole }, 2); + var roleList = IsSpa ? GetMenu2(AllModules, "FrameworkRole", "MenuKey.RoleManagement", new List { adminRole }, 3) : GetMenu(AllModules, "_Admin", "FrameworkRole", "Index", "MenuKey.RoleManagement", new List { adminRole }, 3); + var groupList = IsSpa ? GetMenu2(AllModules, "FrameworkGroup", "MenuKey.GroupManagement", new List { adminRole }, 4) : GetMenu(AllModules, "_Admin", "FrameworkGroup", "Index", "MenuKey.GroupManagement", new List { adminRole }, 4); + var menuList = IsSpa ? GetMenu2(AllModules, "FrameworkMenu", "MenuKey.MenuMangement", new List { adminRole }, 5) : GetMenu(AllModules, "_Admin", "FrameworkMenu", "Index", "MenuKey.MenuMangement", new List { adminRole }, 5); + var dpList = IsSpa ? GetMenu2(AllModules, "DataPrivilege", "MenuKey.DataPrivilege", new List { adminRole }, 6) : GetMenu(AllModules, "_Admin", "DataPrivilege", "Index", "MenuKey.DataPrivilege", new List { adminRole }, 6); + if (logList != null) + { + var menus = new FrameworkMenu[] { logList, userList, roleList, groupList, menuList, dpList }; + foreach (var item in menus) + { + if (item != null) + { + systemManagement.Children.Add(item); + } + } + Set().Add(systemManagement); + + if (IsSpa == false) + { + systemManagement.Icon = "layui-icon layui-icon-set"; + logList?.SetPropertyValue("Icon", "layui-icon layui-icon-form"); + userList?.SetPropertyValue("Icon", "layui-icon layui-icon-friends"); + roleList?.SetPropertyValue("Icon", "layui-icon layui-icon-user"); + groupList?.SetPropertyValue("Icon", "layui-icon layui-icon-group"); + menuList?.SetPropertyValue("Icon", "layui-icon layui-icon-menu-fill"); + dpList?.SetPropertyValue("Icon", "layui-icon layui-icon-auz"); + + var apifolder = GetFolderMenu("Api", new List { adminRole }); + apifolder.ShowOnMenu = false; + apifolder.DisplayOrder = 100; + var logList2 = GetMenu2(AllModules, "ActionLog", "MenuKey.ActionLog", new List { adminRole }, 1); + var userList2 = GetMenu2(AllModules, "FrameworkUser", "MenuKey.UserManagement", new List { adminRole }, 2); + var roleList2 = GetMenu2(AllModules, "FrameworkRole", "MenuKey.RoleManagement", new List { adminRole }, 3); + var groupList2 = GetMenu2(AllModules, "FrameworkGroup", "MenuKey.GroupManagement", new List { adminRole }, 4); + var menuList2 = GetMenu2(AllModules, "FrameworkMenu", "MenuKey.MenuMangement", new List { adminRole }, 5); + var dpList2 = GetMenu2(AllModules, "DataPrivilege", "MenuKey.DataPrivilege", new List { adminRole }, 6); + var apis = new FrameworkMenu[] { logList2, userList2, roleList2, groupList2, menuList2, dpList2 }; + //apis.ToList().ForEach(x => { x.ShowOnMenu = false;x.PageName += $"({Program._localizer["BuildinApi"]})"; }); + foreach (var item in apis) + { + if (item != null) + { + apifolder.Children.Add(item); + + } + } + Set().Add(apifolder); + } + else + { + systemManagement.Icon = " _wtmicon _wtmicon-icon_shezhi"; + logList?.SetPropertyValue("Icon", " _wtmicon _wtmicon-chaxun"); + userList?.SetPropertyValue("Icon", " _wtmicon _wtmicon-zhanghaoquanxianguanli"); + roleList?.SetPropertyValue("Icon", " _wtmicon _wtmicon-quanxianshenpi"); + groupList?.SetPropertyValue("Icon", " _wtmicon _wtmicon-zuzhiqunzu"); + menuList?.SetPropertyValue("Icon", " _wtmicon _wtmicon--lumingpai"); + dpList?.SetPropertyValue("Icon", " _wtmicon _wtmicon-anquan"); + } + } + + } + Set().AddRange(roles); + await SaveChangesAsync(); + } + return rv; + } + + private FrameworkMenu GetFolderMenu(string FolderText, List allowedRoles, bool isShowOnMenu = true, bool isInherite = false) + { + FrameworkMenu menu = new FrameworkMenu + { + PageName = "MenuKey." + FolderText, + Children = new List(), + Privileges = new List(), + ShowOnMenu = isShowOnMenu, + IsInside = true, + FolderOnly = true, + IsPublic = false, + DisplayOrder = 1 + }; + + if (allowedRoles != null) + { + foreach (var role in allowedRoles) + { + menu.Privileges.Add(new FunctionPrivilege { RoleCode = role.RoleCode, Allowed = true }); + + } + } + return menu; + } + + private FrameworkMenu GetMenu(List allModules, string areaName, string controllerName, string actionName, string pageKey, List allowedRoles, int displayOrder) + { + var acts = allModules.Where(x => x.ClassName == controllerName && (areaName == null || x.Area?.Prefix?.ToLower() == areaName.ToLower())).SelectMany(x => x.Actions).ToList(); + var act = acts.Where(x => x.MethodName == actionName).SingleOrDefault(); + var rest = acts.Where(x => x.MethodName != actionName && x.IgnorePrivillege == false).ToList(); + FrameworkMenu menu = GetMenuFromAction(act, true, allowedRoles, displayOrder); + if (menu != null) + { + menu.PageName = pageKey; + for (int i = 0; i < rest.Count; i++) + { + if (rest[i] != null) + { + var sub = GetMenuFromAction(rest[i], false, allowedRoles, (i + 1)); + sub.PageName = pageKey; + menu.Children.Add(sub); + } + } + } + return menu; + } + + private FrameworkMenu GetMenu2(List allModules, string controllerName, string pageKey, List allowedRoles, int displayOrder) + { + var acts = allModules.Where(x => x.FullName == $"WalkingTec.Mvvm.Admin.Api,{controllerName}" && x.IsApi == true).SelectMany(x => x.Actions).ToList(); + var rest = acts.Where(x => x.IgnorePrivillege == false).ToList(); + SimpleAction act = null; + if (acts.Count > 0) + { + act = acts[0]; + } + FrameworkMenu menu = GetMenuFromAction(act, true, allowedRoles, displayOrder); + if (menu != null) + { + menu.PageName = pageKey; + menu.Url = "/" + acts[0].Module.ClassName.ToLower(); + menu.ActionName = "MainPage"; + menu.ClassName = acts[0].Module.FullName; + menu.MethodName = null; + for (int i = 0; i < rest.Count; i++) + { + if (rest[i] != null) + { + var sub = GetMenuFromAction(rest[i], false, allowedRoles, (i + 1)); + sub.PageName = pageKey; + menu.Children.Add(sub); + } + } + } + return menu; + } + + private FrameworkMenu GetMenuFromAction(SimpleAction act, bool isMainLink, List allowedRoles, int displayOrder = 1) + { + if (act == null) + { + return null; + } + FrameworkMenu menu = new FrameworkMenu + { + //ActionId = act.ID, + //ModuleId = act.ModuleId, + ClassName = act.Module.FullName, + MethodName = act.MethodName, + Url = act.Url, + Privileges = new List(), + ShowOnMenu = isMainLink, + FolderOnly = false, + Children = new List(), + IsPublic = false, + IsInside = true, + DisplayOrder = displayOrder, + }; + if (isMainLink) + { + menu.ModuleName = act.Module.ModuleName; + menu.ActionName = act.ActionDes?.Description ?? act.ActionName; + menu.MethodName = null; + } + else + { + menu.ModuleName = act.Module.ModuleName; + menu.ActionName = act.ActionDes?.Description ?? act.ActionName; + } + if (allowedRoles != null) + { + foreach (var role in allowedRoles) + { + menu.Privileges.Add(new FunctionPrivilege { RoleCode = role.RoleCode, Allowed = true }); + + } + } + return menu; + } + + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Enums.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Enums.cs new file mode 100644 index 0000000..2058b09 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Enums.cs @@ -0,0 +1,156 @@ +using System.ComponentModel.DataAnnotations; + +namespace WalkingTec.Mvvm.Core +{ + public enum HttpMethodEnum + { + GET, + POST, + PUT, + DELETE + } + + + /// + /// 列表操作列类型 + /// + public enum ColumnFormatTypeEnum + { + Dialog,//弹出窗口 + Button,//按钮 + Download,//下载 + ViewPic,//查看图片 + Script,//脚本 + Html + } + + + /// + /// 数据库类型 + /// + public enum DBTypeEnum { SqlServer, MySql, PgSql, Memory, SQLite, Oracle } + + /// + /// 页面显示方式 + /// + public enum PageModeEnum { Single, Tab } + + /// + /// Tab页的显示方式 + /// + public enum TabModeEnum { Default, Simple } + + public enum BlazorModeEnum { Server, Wasm} + + /// + /// 按钮 + /// + public enum ButtonTypesEnum + { + Button, + Link + }; + + /// + /// 按钮 + /// + public enum RedirectTypesEnum + { + Layer, + Self, + NewWindow, + NewTab, + }; + + /// + /// 日期类型 + /// + public enum DateTimeTypeEnum + { + /// + /// 日期选择器 + /// 可选择:年、月、日 + /// + Date, + /// + /// 日期时间选择器 + /// 可选择:年、月、日、时、分、秒 + /// + DateTime, + /// + /// 年选择器 + /// 只提供年列表选择 + /// + Year, + /// + /// 年月选择器 + /// 只提供年、月选择 + /// + Month, + /// + /// 时间选择器 + /// 只提供时、分、秒选择 + /// + Time + }; + + /// + /// 图形枚举 + /// + public enum ChartEnum + { + line, + pie, + column, + bubble, + barcolumn + } + + /// + /// 图形统计值类型 + /// + public enum ChartValueType + { + sum, + count, + sumpct, + countpct + } + /// + /// 图形统计分区类型 + /// + public enum PartitionType + { + year, + month, + day, + hour, + minute, + second + } + + public enum UIEnum + { LayUI, React, VUE,Blazor } + + + + public enum BoolComboTypes { YesNo, ValidInvalid, MaleFemale, HaveNotHave, Custom } + + public enum SortDir { Asc, Desc } + + public enum BackgroudColorEnum + { + Grey, + Yellow, + Red + }; + + public enum GenderEnum + { + [Display(Name = "Sys.Male")] + Male = 0, + [Display(Name = "Sys.Female")] + Female = 1 + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Exceptions/NullOrEmptyStringException.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Exceptions/NullOrEmptyStringException.cs new file mode 100644 index 0000000..a343314 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Exceptions/NullOrEmptyStringException.cs @@ -0,0 +1,17 @@ +using System; +using System.Runtime.Serialization; + +namespace WalkingTec.Mvvm.Core.Exceptions +{ + /// + /// 对象为 null 或 空字符串 异常 + /// + public class NullOrEmptyStringException : Exception + { + public const string DEFAULT_EXCEPTION_MSG = "对象为 null 或 空字符串 异常"; + public NullOrEmptyStringException() : base(DEFAULT_EXCEPTION_MSG) { } + public NullOrEmptyStringException(string message) : base(message) { } + public NullOrEmptyStringException(string message, Exception innerException) : base(message, innerException) { } + protected NullOrEmptyStringException(SerializationInfo info, StreamingContext context) : base(info, context) { } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/ConfigExtension.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/ConfigExtension.cs new file mode 100644 index 0000000..8b0b915 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/ConfigExtension.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace WalkingTec.Mvvm.Core.Extensions +{ + public static class ConfigExtension + { + /// + /// + /// + /// + /// + /// + /// + /// + public static IConfigurationBuilder WTMConfig(this IConfigurationBuilder configBuilder, IHostEnvironment env, string jsonFileDir=null, string jsonFileName = null) + { + IConfigurationBuilder rv = configBuilder; + if (string.IsNullOrEmpty(jsonFileDir)) + { + rv = rv.WTM_SetCurrentDictionary(); + } + else + { + rv = rv.SetBasePath(jsonFileDir); + } + if (string.IsNullOrEmpty(jsonFileName)) + { + rv = rv.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); + } + else + { + rv = rv.AddJsonFile(jsonFileName, optional: true, reloadOnChange: true); + } + rv = rv.AddEnvironmentVariables(); + if (env != null) + { + rv = rv.AddInMemoryCollection(new Dictionary { { "HostRoot", env.ContentRootPath } }); + } + else + { + rv = rv.AddInMemoryCollection(new Dictionary { { "HostRoot", Directory.GetCurrentDirectory() } }); + } + return rv; + } + + + public static IConfigurationBuilder WTM_SetCurrentDictionary(this IConfigurationBuilder cb) + { + CurrentDirectoryHelpers.SetCurrentDirectory(); + + if (!File.Exists(Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json"))) + { + var binLocation = Assembly.GetEntryAssembly()?.Location; + if (!string.IsNullOrEmpty(binLocation)) + { + var binPath = new FileInfo(binLocation).Directory?.FullName; + if (File.Exists(Path.Combine(binPath, "appsettings.json"))) + { + Directory.SetCurrentDirectory(binPath); + cb.SetBasePath(binPath); + //.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + //.AddEnvironmentVariables(); + } + } + } + else + { + cb.SetBasePath(Directory.GetCurrentDirectory()); + } + return cb; + } + + } + + /// + /// 解决IIS InProgress下CurrentDirectory获取错误的问题 + /// + internal class CurrentDirectoryHelpers + + { + + internal const string AspNetCoreModuleDll = "aspnetcorev2_inprocess.dll"; + + + + [System.Runtime.InteropServices.DllImport("kernel32.dll")] + + private static extern IntPtr GetModuleHandle(string lpModuleName); + + + + [System.Runtime.InteropServices.DllImport(AspNetCoreModuleDll)] + + private static extern int http_get_application_properties(ref IISConfigurationData iiConfigData); + + + + [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)] + + private struct IISConfigurationData + + { + + public IntPtr pNativeApplication; + + [System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.BStr)] + + public string pwzFullApplicationPath; + + [System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.BStr)] + + public string pwzVirtualApplicationPath; + + public bool fWindowsAuthEnabled; + + public bool fBasicAuthEnabled; + + public bool fAnonymousAuthEnable; + + } + + + + public static void SetCurrentDirectory() + + { + + try + + { + + // Check if physical path was provided by ANCM + + var sitePhysicalPath = Environment.GetEnvironmentVariable("ASPNETCORE_IIS_PHYSICAL_PATH"); + + if (string.IsNullOrEmpty(sitePhysicalPath)) + + { + + // Skip if not running ANCM InProcess + + if (GetModuleHandle(AspNetCoreModuleDll) == IntPtr.Zero) + + { + + return; + + } + + + + IISConfigurationData configurationData = default(IISConfigurationData); + + if (http_get_application_properties(ref configurationData) != 0) + + { + + return; + + } + + + + sitePhysicalPath = configurationData.pwzFullApplicationPath; + + } + + + + Environment.CurrentDirectory = sitePhysicalPath; + + } + + catch + + { + + // ignore + + } + + } + + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/DCExtension.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/DCExtension.cs new file mode 100644 index 0000000..41ca0e0 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/DCExtension.cs @@ -0,0 +1,1112 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Transactions; +using Microsoft.EntityFrameworkCore; +using WalkingTec.Mvvm.Core.Support.Json; + +namespace WalkingTec.Mvvm.Core.Extensions +{ + /// + /// DC相关扩展函数 + /// + public static class DCExtension + { + #region 树形下拉 + /// + /// 查询数据源,并转化成TreeSelectListItem列表 + /// + /// 数据源类型 + /// 基础查询 + /// wtm context + /// 表达式用来获取Text字段对应的值 + /// 表达式用来获取Value字段对应的值,不指定则默认使用Id字段 + /// 表达式用来获取icon字段对应的值 + /// 表达式用来获取Url字段对应的值 + /// 表达式用来获取Tag字段对应的值 + /// 表达式用来获取Expanded字段对应的值,指示节点是否展开 + /// 忽略数据权限判断 + /// 是否根据Text字段排序,默认为是 + /// SelectListItem列表 + public static List GetTreeSelectListItems(this IQueryable baseQuery + , WTMContext wtmcontext + , Expression> textField + , Expression> valueField = null + , Expression> iconField = null + , Expression> urlField = null + , Expression> tagField = null + , Expression> expandField = null + , bool ignorDataPrivilege = false + , bool SortByName = true) + where T : TreePoco + { + var dps = wtmcontext?.LoginUserInfo?.DataPrivileges; + var query = baseQuery.AsNoTracking(); + + //如果没有指定忽略权限,则拼接权限过滤的where条件 + if (ignorDataPrivilege == false) + { + query = AppendSelfDPWhere(query, wtmcontext, dps); + } + + if (typeof(IPersistPoco).IsAssignableFrom(typeof(T))) + { + var mod = new IsValidModifier(); + var newExp = mod.Modify(query.Expression); + query = query.Provider.CreateQuery(newExp) as IOrderedQueryable; + } + + //处理后面要使用的expression + //if (valueField == null) + //{ + valueField = x => x.GetID().ToString(); + //} + Expression> parentField = x => x.GetParentID().ToString(); + + //定义PE + ParameterExpression pe = Expression.Parameter(typeof(T)); + ChangePara cp = new ChangePara(); + + //创建新类,形成类似 new SimpleTreeTextAndValue() 的表达式 + NewExpression newItem = Expression.New(typeof(TreeSelectListItem)); + + //绑定Text字段,形成类似 Text = textField 的表达式 + var textMI = typeof(TreeSelectListItem).GetMember("Text")[0]; + MemberBinding textBind = Expression.Bind(textMI, cp.Change(textField.Body, pe)); + + //绑定Value字段,形成类似 Value = valueField 的表达式 + var valueMI = typeof(TreeSelectListItem).GetMember("Value")[0]; + MemberBinding valueBind = Expression.Bind(valueMI, cp.Change(valueField.Body, pe)); + + //绑定ParentId字段,形成类似 Value = valueField 的表达式 + var parentMI = typeof(TreeSelectListItem).GetMember("ParentId")[0]; + MemberBinding parentBind = Expression.Bind(parentMI, cp.Change(parentField.Body, pe)); + + //绑定Url字段,形成类似 Value = valueField 的表达式 + MemberBinding urlBind = null; + var urlMI = typeof(TreeSelectListItem).GetMember("Url")[0]; + if (urlField != null) + { + urlBind = Expression.Bind(urlMI, cp.Change(urlField.Body, pe)); + } + else + { + urlBind = Expression.Bind(urlMI, Expression.Constant(string.Empty)); + } + + //绑定icon字段,形成类似 Icon = iconField 的表达式 + MemberBinding iconBind = null; + var iconMI = typeof(TreeSelectListItem).GetMember("Icon")[0]; + if (iconField != null) + { + iconBind = Expression.Bind(iconMI, cp.Change(iconField.Body, pe)); + } + else + { + iconBind = Expression.Bind(iconMI, Expression.Constant(string.Empty)); + } + + //绑定Tag字段,形成类似 Value = valueField 的表达式 + MemberBinding tagBind = null; + var tagMI = typeof(TreeSelectListItem).GetMember("Tag")[0]; + if (tagField != null) + { + tagBind = Expression.Bind(tagMI, cp.Change(tagField.Body, pe)); + } + else + { + tagBind = Expression.Bind(tagMI, Expression.Constant("")); + } + + //绑定Tag字段,形成类似 Value = valueField 的表达式 + MemberBinding expandBind = null; + var expandMI = typeof(TreeSelectListItem).GetMember("Expended")[0]; + if (expandField != null) + { + expandBind = Expression.Bind(expandMI, cp.Change(expandField.Body, pe)); + } + else + { + expandBind = Expression.Bind(expandMI, Expression.Constant(false)); + } + + //合并创建新类和绑定字段的表达式,形成类似 new SimpleTextAndValue{ Text = textField, Value = valueField} 的表达式 + MemberInitExpression init = Expression.MemberInit(newItem, textBind, valueBind, iconBind, parentBind, urlBind, tagBind, expandBind); + + //将最终形成的表达式转化为Lambda,形成类似 x=> new SimpleTextAndValue { Text = x.textField, Value = x.valueField} 的表达式 + var lambda = Expression.Lambda>(init, pe); + + List rv = null; + + //根据Text对下拉菜单数据排序 + if (SortByName == true) + { + rv = query.Select(lambda).ToList().OrderBy(x => x.Text).ToList(); + } + else + { + rv = query.Select(lambda).ToList(); + } + + List toDel = new List(); + + rv.ForEach(x => + { + var c = rv.Where(y => y.ParentId == x.Value.ToString()).ToList(); + x.Children = c; + toDel.AddRange(c); + }); + toDel.ForEach(x => rv.Remove(x)); + return rv.ToList(); + } + + #endregion + + #region 下拉 + /// + /// 查询数据源,并转化成SelectListItem列表 + /// + /// 数据源类型 + /// 基础查询 + /// Wtm Context + /// SelectListItem中Text字段对应的值 + /// SelectListItem中Value字段对应的值,默认为Id列 + /// 忽略数据权限判断 + /// 是否根据Text字段排序,默认为是 + /// SelectListItem列表 + public static List GetSelectListItems(this IQueryable baseQuery + , WTMContext wtmcontext + , Expression> textField + , Expression> valueField = null + , bool ignorDataPrivilege = false + , bool SortByName = true) + where T : TopBasePoco + { + var dps = wtmcontext?.LoginUserInfo?.DataPrivileges; + var query = baseQuery.AsNoTracking(); + + //如果value字段为空,则默认使用Id字段作为value值 + if (valueField == null) + { + valueField = x => x.GetID().ToString(); + } + + //如果没有指定忽略权限,则拼接权限过滤的where条件 + if (ignorDataPrivilege == false) + { + query = AppendSelfDPWhere(query,wtmcontext,dps); + } + + if (typeof(IPersistPoco).IsAssignableFrom( typeof(T))) + { + var mod = new IsValidModifier(); + var newExp = mod.Modify(query.Expression); + query = query.Provider.CreateQuery(newExp) as IOrderedQueryable; + } + + + //定义PE + ParameterExpression pe = Expression.Parameter(typeof(T)); + ChangePara cp = new ChangePara(); + //创建新类,形成类似 new SimpleTextAndValue() 的表达式 + NewExpression newItem = Expression.New(typeof(ComboSelectListItem)); + + //绑定Text字段,形成类似 Text = textField 的表达式 + var textMI = typeof(ComboSelectListItem).GetMember("Text")[0]; + MemberBinding textBind = Expression.Bind(textMI, cp.Change(textField.Body, pe)); + + + //绑定Value字段,形成类似 Value = valueField 的表达式 + var valueMI = typeof(ComboSelectListItem).GetMember("Value")[0]; + MemberBinding valueBind = Expression.Bind(valueMI, cp.Change(valueField.Body, pe)); + + //如果是树形结构,给ParentId赋值 + MemberBinding parentBind = null; + var parentMI = typeof(ComboSelectListItem).GetMember("ParentId")[0]; + if (typeof(TreePoco<>).IsAssignableFrom(typeof(T))) + { + var parentMember = Expression.MakeMemberAccess(pe, typeof(TreePoco<>).GetSingleProperty("ParentId")); + var p = Expression.Call(parentMember, "ToString", new Type[] { }); + //var p1 = Expression.Call(p, "ToLower", new Type[] { }); + parentBind = Expression.Bind(parentMI, p); + } + else + { + parentBind = Expression.Bind(parentMI, Expression.Constant(string.Empty)); + } + + //合并创建新类和绑定字段的表达式,形成类似 new SimpleTextAndValue{ Text = textField, Value = valueField} 的表达式 + MemberInitExpression init = Expression.MemberInit(newItem, textBind, valueBind, parentBind); + + //将最终形成的表达式转化为Lambda,形成类似 x=> new SimpleTextAndValue { Text = x.textField, Value = x.valueField} 的表达式 + var lambda = Expression.Lambda>(init, pe); + + + List rv = new List(); + //根据Text对下拉菜单数据排序 + if (SortByName == true) + { + rv = query.Select(lambda).ToList().OrderBy(x => x.Text).ToList(); + } + else + { + rv = query.Select(lambda).ToList(); + } + + return rv; + } + + #endregion + + /// + /// 拼接本表的数据权限过滤 + /// + /// 数据类 + /// 源query + /// Wtm context + /// 数据权限列表 + /// 拼接好where条件的query + private static IQueryable AppendSelfDPWhere(IQueryable query, WTMContext wtmcontext, List dps) where T : TopBasePoco + { + var dpsSetting = wtmcontext?.DataPrivilegeSettings; + Type modelTye = typeof(T); + bool isBasePoco = typeof(IBasePoco).IsAssignableFrom(modelTye); + ParameterExpression pe = Expression.Parameter(modelTye); + Expression peid = Expression.Property(pe, modelTye.GetSingleProperty("ID")); + //循环数据权限,加入到where条件中,达到自动过滤的效果 + + Expression selfexp = Expression.NotEqual(Expression.Constant(1), Expression.Constant(1)); + if(isBasePoco == true) + { + selfexp = Expression.Equal(Expression.Property(pe, "CreateBy"), Expression.Constant(wtmcontext.LoginUserInfo?.ITCode)); + } + if (dpsSetting?.Where(x => x.ModelName == query.ElementType.Name).SingleOrDefault() != null) + { + //如果dps参数是空,则生成 1!=1 这种错误的表达式,这样就查不到任何数据了 + if (dps == null) + { + query = query.Where(Expression.Lambda>(selfexp, pe)); + } + else + { + //在dps中找到和baseQuery源数据表名一样的关联id + var ids = dps.Where(x => x.TableName == query.ElementType.Name).Select(x => x.RelateId).ToList(); + if (ids == null || ids.Count() == 0) + { + query = query.Where(Expression.Lambda>(selfexp, pe)); + } + else + { + if (!ids.Contains(null)) + { + var exp = Expression.OrElse(selfexp, ids.GetContainIdExpression(typeof(T), pe).Body); + query = query.Where(Expression.Lambda>(exp,pe)); + } + } + } + } + return query; + } + + /// + /// 为查询语句添加关联表的权限过滤 + /// + /// 源数据类 + /// 源Query + /// + /// 关联表外键 + /// 修改后的查询语句 + public static IQueryable DPWhere(this IQueryable baseQuery, WTMContext wtmcontext, params Expression>[] IdFields) where T:TopBasePoco + { + var dps = wtmcontext?.LoginUserInfo?.DataPrivileges; + //循环所有关联外键 + List tableNameList = new List(); + foreach (var IdField in IdFields) + { + + //将外键 Id 用.分割,循环生成指向最终id的表达式,比如x=> x.a.b.Id + var fieldName = IdField.GetPropertyName(false); + //获取关联的类 + string typename = ""; + //如果外键名称不是‘id’,则根据model层的命名规则,它应该是xxxId,所以抹掉最后的 Id 应该是关联的类名 + if (fieldName.ToLower() != "id") + { + fieldName = fieldName.Remove(fieldName.Length - 2); + typename = IdField.GetPropertyInfo().DeclaringType.GetSingleProperty(fieldName).PropertyType.Name; + } + //如果是 Id,则本身就是关联的类 + else + { + typename = typeof(T).Name; + } + tableNameList.Add(typename); + + } + //var test = DPWhere(baseQuery, dps, tableNameList, IdFields); + return DPWhere(baseQuery,wtmcontext,tableNameList, IdFields); + } + + #region AddBy YOUKAI 20160310 + /// + /// 为查询语句添加关联表的权限过滤 + /// + /// 源数据类 + /// 源Query + /// wtm context + /// 关联数据权限的表名,如果关联外键为自身,则参数第一个为自身 + /// 关联表外键 + /// 修改后的查询语句 + public static IQueryable DPWhere(this IQueryable baseQuery,WTMContext wtmcontext,List tableName, params Expression>[] IdFields) where T:TopBasePoco + { + var dps = wtmcontext?.LoginUserInfo?.DataPrivileges; + Type modelTye = typeof(T); + bool isBasePoco = typeof(IBasePoco).IsAssignableFrom(modelTye); + + // var dpsSetting = BaseVM.AllDPS; + ParameterExpression pe = Expression.Parameter(modelTye); + Expression left1 = Expression.Constant(1); + Expression right1 = Expression.Constant(1); + Expression trueExp = Expression.Equal(left1, right1); + Expression falseExp = Expression.NotEqual(left1, right1); + Expression finalExp = null; + Expression selfexp = falseExp; + if (isBasePoco == true) + { + selfexp = Expression.Equal(Expression.Property(pe, "CreateBy"), Expression.Constant(wtmcontext.LoginUserInfo?.ITCode)); + } + int tindex = 0; + //循环所有关联外键 + foreach (var IdField in IdFields) + { + bool mtm = false; + Expression exp = trueExp; + //将外键Id用.分割,循环生成指向最终id的表达式,比如x=> x.a.b.Id + var fullname = IdField.GetPropertyName(); + string[] splits = fullname.Split('.'); + int leftindex = splits[0].IndexOf('['); + if (leftindex > 0) + { + mtm = true; + splits[0] = splits[0].Substring(0, leftindex); + } + Expression peid = Expression.MakeMemberAccess(pe, pe.Type.GetSingleProperty(splits[0])); + Type middletype = null; + if (mtm) + { + middletype = peid.Type.GetGenericArguments()[0]; + + } + else + { + for (int i = 1; i < splits.Length; i++) + { + peid = Expression.MakeMemberAccess(peid, peid.Type.GetSingleProperty(splits[i])); + } + middletype = (peid as MemberExpression).Member.DeclaringType; + } + //如果dps为空,则拼接一个返回假的表达式,这样就查询不出任何数据 + if (dps == null) + { + exp = selfexp; + } + else + { + var fieldName = IdField.GetPropertyName(false); + //如果外键名称不是‘id’,则根据model层的命名规则,它应该是xxxId,所以抹掉最后的 Id 应该是关联的类名 + if (fieldName.ToLower() != "id") + { + fieldName = fieldName.Remove(fieldName.Length - 2); + var typeinfo = middletype.GetSingleProperty(fieldName); + //var IsTableName = tableName?.Where(x => x == fieldName).FirstOrDefault(); + var IsTableName = tableName?.Where(x => x.ToLower() == typeinfo.PropertyType.Name.ToLower()).FirstOrDefault(); + if (string.IsNullOrEmpty(IsTableName)) + { + continue; + } + fieldName = IsTableName; + //typename = PropertyHelper.GetPropertyInfo(IdField).DeclaringType.GetProperty(fieldName).PropertyType.Name; + } + //如果是Id,则本身就是关联的类 + else + { + fieldName = tableName[tindex]; + } + var dpsSetting = wtmcontext.DataPrivilegeSettings; + + //循环系统设定的数据权限,如果没有和关联类一样的表,则跳过 + if (dpsSetting.Where(x => x.ModelName == fieldName).SingleOrDefault() == null) + { + continue; + } + //获取dps中关联到关联类的id列表 + var ids = dps.Where(x => x.TableName == fieldName).Select(x => x.RelateId).ToList(); + //如果没有关联的id,则拼接一个返回假的where,是语句查询不到任何数据 + if (ids == null || ids.Count() == 0) + { + exp = selfexp; + } + //如果有关联 Id + else + { + //如果关联 Id 不包含null,则生成类似 x=> ids.Contains(x.a.b.Id) 这种条件 + //如果关联 Id 包括null,则代表可以访问所有数据,就不需要再拼接where条件了 + if (!ids.Contains(null)) + { + if (mtm == true) + { + ParameterExpression midpe = Expression.Parameter(middletype); + Expression middleid = Expression.PropertyOrField(midpe, IdField.GetPropertyName(false)); + + var queryable = Expression.Call( + typeof(Queryable), + "AsQueryable", + new Type[] { middletype }, + peid); + + List ddd = new List(); + + exp = Expression.Call( + typeof(Queryable), + "Any", + new Type[] { middletype }, + queryable, + Expression.Lambda(typeof(Func<,>).MakeGenericType(middletype, typeof(bool)), ids.GetContainIdExpression(middletype, midpe, middleid).Body, new ParameterExpression[] { midpe })); + + } + else + { + exp = ids.GetContainIdExpression(typeof(T), pe, peid).Body; + } + exp = Expression.OrElse(selfexp, exp); + } + } + } + //把所有where里的条件用And拼接在一起 + if (finalExp == null) + { + finalExp = exp; + } + else + { + finalExp = Expression.OrElse(finalExp, exp); + } + tindex++; + } + //如果没有进行任何修改,则还返回baseQuery + if (finalExp == null) + { + return baseQuery; + } + else + { + //返回加入了where条件之后的baseQuery + var query = baseQuery.Where(Expression.Lambda>(finalExp, pe)); + return query; + } + } + #endregion + + public static IOrderedQueryable Sort(this IQueryable baseQuery, string sortInfo, params SortInfo[] defaultSorts) where T : TopBasePoco + { + List info = new List(); + IOrderedQueryable rv = null; + if (string.IsNullOrEmpty(sortInfo)) + { + if (defaultSorts == null || defaultSorts.Length == 0) + { + ParameterExpression pe = Expression.Parameter(typeof(T)); + var idproperty = typeof(T).GetSingleProperty("ID"); + Expression pro = Expression.Property(pe, idproperty); + Type proType = typeof(Guid); + Expression final = Expression.Call( + typeof(Queryable), + "OrderBy", + new Type[] { typeof(T), proType }, + baseQuery.Expression, + Expression.Lambda(pro, new ParameterExpression[] { pe })); + rv = baseQuery.Provider.CreateQuery(final) as IOrderedQueryable; + return rv; + } + else + { + info.AddRange(defaultSorts); + } + } + else + { + var temp = JsonSerializer.Deserialize>(sortInfo); + info.AddRange(temp); + } + foreach (var item in info) + { + ParameterExpression pe = Expression.Parameter(typeof(T)); + var idproperty = typeof(T).GetSingleProperty(item.Property); + Expression pro = Expression.Property(pe, idproperty); + Type proType = typeof(T).GetSingleProperty(item.Property).PropertyType; + if (item.Direction == SortDir.Asc) + { + if (rv == null) + { + Expression final = Expression.Call( + typeof(Queryable), + "OrderBy", + new Type[] { typeof(T), proType }, + baseQuery.Expression, + Expression.Lambda(pro, new ParameterExpression[] { pe })); + rv = baseQuery.Provider.CreateQuery(final) as IOrderedQueryable; + } + else + { + Expression final = Expression.Call( + typeof(Queryable), + "ThenBy", + new Type[] { typeof(T), proType }, + rv.Expression, + Expression.Lambda(pro, new ParameterExpression[] { pe })); + rv = rv.Provider.CreateQuery(final) as IOrderedQueryable; + } + } + if (item.Direction == SortDir.Desc) + { + if (rv == null) + { + Expression final = Expression.Call( + typeof(Queryable), + "OrderByDescending", + new Type[] { typeof(T), proType }, + baseQuery.Expression, + Expression.Lambda(pro, new ParameterExpression[] { pe })); + rv = baseQuery.Provider.CreateQuery(final) as IOrderedQueryable; + } + else + { + Expression final = Expression.Call( + typeof(Queryable), + "ThenByDescending", + new Type[] { typeof(T), proType }, + rv.Expression, + Expression.Lambda(pro, new ParameterExpression[] { pe })); + rv = rv.Provider.CreateQuery(final) as IOrderedQueryable; + } + } + } + return rv; + } + + public static IQueryable CheckID(this IQueryable baseQuery, object val, Expression> member=null) + { + ParameterExpression pe = Expression.Parameter(typeof(T)); + PropertyInfo idproperty = null; + if (member == null) + { + idproperty = typeof(T).GetSingleProperty("ID"); + } + else + { + idproperty = member.GetPropertyInfo(); + } + Expression peid = Expression.Property(pe, idproperty); + var convertid = PropertyHelper.ConvertValue(val, idproperty.PropertyType); + return baseQuery.Where(Expression.Lambda>(Expression.Equal(peid, Expression.Constant(convertid)), pe)); + } + + public static IQueryable CheckParentID(this IQueryable baseQuery, string val) + { + ParameterExpression pe = Expression.Parameter(typeof(T)); + PropertyInfo idproperty = null; + idproperty = typeof(T).GetSingleProperty("ParentId"); + Expression peid = Expression.Property(pe, idproperty); + var p = Expression.Call(peid, "ToString", new Type[] { }); + if(val == null) + { + return baseQuery.Where(Expression.Lambda>(Expression.Equal(peid, Expression.Constant(null)), pe)); + } + else + { + return baseQuery.Where(Expression.Lambda>(Expression.Equal(p, Expression.Constant(val)), pe)); + } + } + + + public static IQueryable CheckIDs(this IQueryable baseQuery, List val, Expression> member = null) + { + if(val == null) + { + return baseQuery; + } + ParameterExpression pe = Expression.Parameter(typeof(T)); + PropertyInfo idproperty = null; + if (member == null) + { + idproperty = typeof(T).GetSingleProperty("ID"); + } + else + { + idproperty = member.GetPropertyInfo(); + } + Expression peid = Expression.Property(pe, idproperty); + var exp = val.GetContainIdExpression(typeof(T), pe, peid).Body; + return baseQuery.Where(Expression.Lambda>(exp, pe)); + } + + + public static IQueryable CheckNotNull(this IQueryable baseQuery, Expression> member) + { + return baseQuery.CheckNotNull(member.GetPropertyName()); + } + + public static IQueryable CheckNotNull(this IQueryable baseQuery, string member) + { + ParameterExpression pe = Expression.Parameter(typeof(T)); + PropertyInfo idproperty = typeof(T).GetSingleProperty(member); + Expression peid = Expression.Property(pe, idproperty); + return baseQuery.Where(Expression.Lambda>(Expression.NotEqual(peid, Expression.Constant(null)), pe)); + } + + + public static IQueryable CheckNull(this IQueryable baseQuery, Expression> member) + { + ParameterExpression pe = Expression.Parameter(typeof(T)); + PropertyInfo idproperty = typeof(T).GetSingleProperty(member.GetPropertyName()); + Expression peid = Expression.Property(pe, idproperty); + return baseQuery.Where(Expression.Lambda>(Expression.Equal(peid, Expression.Constant(null)), pe)); + } + + /// + /// val不为空时,附加查询条件 + /// + /// + /// + /// + /// + /// + /// + public static IQueryable CheckWhere(this IQueryable baseQuery, S val, Expression> where) + { + if (val == null) + { + return baseQuery; + } + else if(val is string s && string.IsNullOrEmpty(s)) + { + return baseQuery; + } + else + { + if (typeof(IList).IsAssignableFrom(val.GetType())) + { + if (((IList)val).Count == 0) + { + return baseQuery; + } + } + return baseQuery.Where(where); + } + } + + /// + /// 条件为true时,附加查询条件 + /// + /// + /// + /// bool? + /// + /// + public static IQueryable WhereIf(this IQueryable baseQuery, bool? val, Expression> where) + { + if (val == null || val == false) + { + return baseQuery; + } + return baseQuery.Where(where); + } + + public static IQueryable CheckEqual(this IQueryable baseQuery, string val, Expression> field) + { + if (val == null || val == "") + { + return baseQuery; + } + else + { + val = val.Trim(); + var equal = Expression.Equal(field.Body, Expression.Constant(val)); + var where = Expression.Lambda>(equal, field.Parameters[0]); + return baseQuery.Where(where); + } + } + + public static IQueryable CheckEqual(this IQueryable baseQuery, S? val, Expression> field) + where S : struct + { + if (val == null) + { + return baseQuery; + } + else + { + var equal = Expression.Equal(Expression.PropertyOrField(field.Body, "Value"), Expression.Constant(val)); + var where = Expression.Lambda>(equal, field.Parameters[0]); + return baseQuery.Where(where); + } + } + + public static IQueryable CheckEqual(this IQueryable baseQuery, S val, Expression> field) + where S : struct + { + S? a = val; + return baseQuery.CheckEqual(a, field); + } + + + public static IQueryable CheckBetween(this IQueryable baseQuery, S? valMin, S? valMax, Expression> field, bool includeMin = true, bool includeMax = true) + where S : struct + { + if (valMin == null && valMax == null) + { + return baseQuery; + } + else + { + IQueryable rv = baseQuery; + if (valMin != null) + { + BinaryExpression exp1 = !includeMin ? Expression.GreaterThan(Expression.PropertyOrField(field.Body, "Value"), Expression.Constant(valMin)) : Expression.GreaterThanOrEqual(Expression.PropertyOrField(field.Body, "Value"), Expression.Constant(valMin)); + rv = rv.Where(Expression.Lambda>(exp1, field.Parameters[0])); + } + if (valMax != null) + { + BinaryExpression exp2 = !includeMax ? Expression.LessThan(Expression.PropertyOrField(field.Body, "Value"), Expression.Constant(valMax)) : Expression.LessThanOrEqual(Expression.PropertyOrField(field.Body, "Value"), Expression.Constant(valMax)); + rv = rv.Where(Expression.Lambda>(exp2, field.Parameters[0])); + } + return rv; + } + } + + public static IQueryable CheckBetween(this IQueryable baseQuery, S valMin, S valMax, Expression> field, bool includeMin = true, bool includeMax = true) +where S : struct + { + S? a = valMin; + S? b = valMax; + return CheckBetween(baseQuery, a, b, field, includeMin, includeMax); + } + + public static IQueryable CheckBetween(this IQueryable baseQuery, S? valMin, S valMax, Expression> field, bool includeMin = true, bool includeMax = true) +where S : struct + { + S? a = valMin; + S? b = valMax; + return CheckBetween(baseQuery, a, b, field, includeMin, includeMax); + } + + public static IQueryable CheckBetween(this IQueryable baseQuery, S valMin, S? valMax, Expression> field, bool includeMin = true, bool includeMax = true) +where S : struct + { + S? a = valMin; + S? b = valMax; + return CheckBetween(baseQuery, a, b, field, includeMin, includeMax); + } + + public static IQueryable CheckContain(this IQueryable baseQuery, string val, Expression> field, bool ignoreCase = true) + { + if (string.IsNullOrEmpty(val)) + { + return baseQuery; + } + else + { + val = val.Trim(); + Expression exp = null; + if (ignoreCase == true) + { + var tolower = Expression.Call(field.Body, "ToLower", null); + exp = Expression.Call(tolower, "Contains", null, Expression.Constant(val.ToLower())); + } + else + { + exp = Expression.Call(field.Body, "Contains", null, Expression.Constant(val)); + + } + var where = Expression.Lambda>(exp, field.Parameters[0]); + return baseQuery.Where(where); + } + } + + public static IQueryable CheckContain(this IQueryable baseQuery, List val, Expression> field) + { + if (val == null || val.Count == 0 || (val.Count == 1 && val[0] == null)) + { + return baseQuery; + } + else + { + Expression exp = null; + exp = Expression.Call(Expression.Constant(val), "Contains", null, field.Body); + + var where = Expression.Lambda>(exp, field.Parameters[0]); + return baseQuery.Where(where); + } + } + + public static IQueryable DynamicSelect(this IQueryable baseQuery, string fieldName) + { + ParameterExpression pe = Expression.Parameter(typeof(T)); + var idproperty = typeof(T).GetSingleProperty(fieldName); + Expression pro = Expression.Property(pe, idproperty); + Expression tostring = Expression.Call(pro, "ToString", new Type[] { }); + Type proType = typeof(string); + Expression final = Expression.Call( + typeof(Queryable), + "Select", + new Type[] { typeof(T), proType }, + baseQuery.Expression, + Expression.Lambda(tostring, new ParameterExpression[] { pe })); + var rv = baseQuery.Provider.CreateQuery(final) as IOrderedQueryable; + return rv; + } + + + public static string GetTableName(this IDataContext self) + { + return self.Model.FindEntityType(typeof(T)).GetTableName(); + } + + /// + /// 通过模型和模型的某个List属性的名称来判断List的字表中关联到主表的主键名称 + /// + /// 主表Model + /// DataContext + /// 主表中的子表List属性名称 + /// 主键名称 + public static string GetFKName(this IDataContext self, string listFieldName) where T : class + { + return GetFKName(self, typeof(T), listFieldName); + } + + /// + /// 通过模型和模型的某个List属性的名称来判断List的字表中关联到主表的主键名称 + /// + /// DataContext + /// 主表model类型 + /// 主表中的子表List属性名称 + /// 主键名称 + public static string GetFKName(this IDataContext self, Type sourceType, string listFieldName) + { + try + { + var test = self.Model.FindEntityType(sourceType).GetReferencingForeignKeys().Where(x => x.PrincipalToDependent?.Name == listFieldName).FirstOrDefault(); + if (test != null && test.Properties.Count > 0) + { + return test.Properties[0].Name; + } + else + { + return ""; + } + } + catch + { + return ""; + } + } + + + /// + /// 通过子表模型和模型关联到主表的属性名称来判断该属性对应的主键名称 + /// + /// 子表Model + /// DataContext + /// 关联主表的属性名称 + /// 主键名称 + public static string GetFKName2(this IDataContext self, string FieldName) where T : class + { + return GetFKName2(self, typeof(T), FieldName); + } + + /// + /// 通过模型和模型关联到主表的属性名称来判断该属性对应的主键名称 + /// + /// DataContext + /// 子表model类型 + /// 关联主表的属性名称 + /// 主键名称 + public static string GetFKName2(this IDataContext self, Type sourceType, string FieldName) + { + try + { + var test = self.Model.FindEntityType(sourceType).GetForeignKeys().Where(x => x.DependentToPrincipal?.Name == FieldName).FirstOrDefault(); + if (test != null && test.Properties.Count > 0) + { + return test.Properties[0].Name; + } + else + { + return ""; + } + } + catch + { + return ""; + } + } + + public static string GetFieldName(this IDataContext self, Expression> field) + { + string pname = field.GetPropertyName(); + return self.GetFieldName(pname); + } + + + public static string GetFieldName(this IDataContext self, string fieldname) + { + var rv = self.Model.FindEntityType(typeof(T)).FindProperty(fieldname); + return rv?.GetColumnName(new Microsoft.EntityFrameworkCore.Metadata.StoreObjectIdentifier()); + } + + public static string GetPropertyNameByFk(this IDataContext self, Type sourceType, string fkname) + { + try + { + var test = self.Model.FindEntityType(sourceType).GetForeignKeys().Where(x => x.DependentToPrincipal?.ForeignKey?.Properties[0]?.Name == fkname).FirstOrDefault(); + if (test != null && test.Properties.Count > 0) + { + return test.DependentToPrincipal.Name; + } + else + { + return ""; + } + } + catch + { + return ""; + } + } + + + public static Expression> GetContainIdExpression(this List Ids, Expression peid = null) + { + ParameterExpression pe = Expression.Parameter(typeof(TModel)); + var rv = Ids.GetContainIdExpression(typeof(TModel), pe, peid) as Expression>; + return rv; + } + + public static LambdaExpression GetContainIdExpression(this List Ids, Type modeltype, ParameterExpression pe, Expression peid = null) + { + if (Ids == null) + { + Ids = new List(); + } + if (peid == null) + { + peid = Expression.Property(pe, modeltype.GetSingleProperty("ID")); + } + else + { + ChangePara cp = new ChangePara(); + peid = cp.Change(peid, pe); + } + List newids = new List(); + foreach (var item in Ids) + { + object vv = PropertyHelper.ConvertValue(item, peid.Type); + if (vv != null) + { + newids.Add(vv); + } + } + Expression dpleft = Expression.Constant(newids, typeof(IEnumerable)); + Expression dpleft2 = Expression.Call(typeof(Enumerable), "Cast", new Type[] { peid.Type }, dpleft); + Expression dpleft3 = Expression.Call(typeof(Enumerable), "ToList", new Type[] { peid.Type }, dpleft2); + Expression dpcondition = Expression.Call(typeof(Enumerable), "Contains", new Type[] { peid.Type }, dpleft3, peid); + var rv = Expression.Lambda(typeof(Func<,>).MakeGenericType(modeltype, typeof(bool)), dpcondition, pe); + return rv; + } + + /// + /// 开始一个事务,当使用同一IDataContext时,嵌套的两个事务不会引起冲突,当嵌套的事务执行时引起的异常会通过回滚方法向上层抛出异常 + /// + /// DataContext + /// 可用的事务实例 + public static Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction BeginTransaction(this IDataContext self) + { + if (self == null) + throw new ArgumentNullException(nameof(self)); + if (self.Database == null) + throw new ArgumentNullException(nameof(self.Database)); + if (@"Microsoft.EntityFrameworkCore.InMemory".Equals(self.Database.ProviderName, StringComparison.OrdinalIgnoreCase)) + return FakeNestedTransaction.DefaultTransaction; + return self.Database.CurrentTransaction == null ? self.Database.BeginTransaction() : FakeNestedTransaction.DefaultTransaction; + } + + /// + /// 开始一个事务,当使用同一IDataContext时,嵌套的两个事务不会引起冲突,当嵌套的事务执行时引起的异常会通过回滚方法向上层抛出异常 + /// + /// DataContext + /// + /// 可用的事务实例 + public static Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction BeginTransaction(this IDataContext self, System.Data.IsolationLevel isolationLevel) + { + if (self == null) + throw new ArgumentNullException(nameof(self)); + if (self.Database == null) + throw new ArgumentNullException(nameof(self.Database)); + if (@"Microsoft.EntityFrameworkCore.InMemory".Equals(self.Database.ProviderName, StringComparison.OrdinalIgnoreCase)) + return FakeNestedTransaction.DefaultTransaction; + return self.Database.CurrentTransaction == null ? self.Database.BeginTransaction(isolationLevel) : FakeNestedTransaction.DefaultTransaction; + } + } + + public static class DbCommandExtension + { + public static void AddParameter(this DbCommand command) + { + + } + } + + internal class FakeNestedTransaction : Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction + { + internal static readonly FakeNestedTransaction DefaultTransaction = new FakeNestedTransaction(); + + private FakeNestedTransaction() { } + + public void Dispose() + { + } + + public void Commit() + { + } + + public void Rollback() + { + throw new TransactionInDoubtException("an exception occurs while executing the nested transaction or processing the results"); + } + + public Task CommitAsync(CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public Task RollbackAsync(CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public ValueTask DisposeAsync() + { + throw new NotImplementedException(); + } + + public Guid TransactionId => Guid.Empty; + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/DistinctExtension.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/DistinctExtension.cs new file mode 100644 index 0000000..9aba5fe --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/DistinctExtension.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Linq; + +namespace WalkingTec.Mvvm.Core.Extensions +{ + public static class DistinctExtensions + { + public static IEnumerable Distinct(this IEnumerable source, Func keySelector) + { + return source.Distinct(new CommonEqualityComparer(keySelector)); + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/ITreeDataExtension.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/ITreeDataExtension.cs new file mode 100644 index 0000000..fa1f0c5 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/ITreeDataExtension.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace WalkingTec.Mvvm.Core.Extensions +{ + /// + /// 树形结构Model的扩展函数 + /// + public static class ITreeDataExtension + { + + /// + /// 获取一个父节点下的所有子节点,包括子节点的子节点 + /// + /// 树形结构类 + /// 树形结构实例 + /// 排序字段,可为空 + /// 树形结构列表,包含所有子节点 + public static List GetAllChildren(this T self, Func order = null) + where T : TreePoco + { + List rv = new List(); + var children = self.Children; + if(order != null && children != null) + { + children = children.OrderBy(order).ToList(); + } + if (children != null && children.Count() > 0) + { + //var dictinct = children.Where(x => x.GetID().ToString() != self.GetID().ToString()).ToList(); + foreach (var item in children) + { + rv.Add(item); + //递归添加子节点的子节点 + rv.AddRange(item.GetAllChildren(order)); + } + } + return rv; + } + + public static int GetLevel(this T self) + where T : TreePoco + { + int level = 0; + while (self.Parent != null) + { + level++; + self = self.Parent; + } + return level; + } + + /// + /// 查询数据库,根据某个节点ID递归获取其下所有级别的子节点ID + /// + /// 树形结构类 + /// 树形结构实例 + /// dc + /// 子节点ID列表 + /// 所有级别子节点ID + public static List GetAllChildrenIDs(this T self + , IDataContext dc + , List subids = null) + where T : TreePoco + { + List rv = new List(); + List ids = null; + if (subids == null) + { + ids = dc.Set().Where(x => x.ParentId == self.ID).Select(x => x.ID).ToList(); + } + else + { + ids = dc.Set().Where(x => subids.Contains(x.ParentId.Value)).Select(x => x.ID).ToList(); + } + if (ids != null && ids.Count > 0) + { + rv.AddRange(ids); + rv.AddRange(self.GetAllChildrenIDs(dc, ids)); + } + return rv; + } + + /// + /// 将树形结构列表转变为标准列表 + /// + /// 树形结构类 + /// 树形结构实例 + /// 排序字段,可以为空 + /// 返回标准列表,所有节点都在同一级上 + public static List FlatTree(this List self, Func order = null) + where T :TreePoco + { + List rv = new List(); + if(order != null) + { + self = self.OrderBy(order).ToList(); + } + foreach (var item in self) + { + rv.Add(item); + rv.AddRange(item.GetAllChildren(order)); + } + return rv; + } + + /// + /// 将树形结构列表转变为标准列表 + /// + /// 树形结构实例 + /// 排序字段,可以为空 + /// 返回标准列表,所有节点都在同一级上 + public static IEnumerable FlatTreeSelectList(this IEnumerable self, Func order = null) + { + List rv = new List(); + if (order != null) + { + self = self.OrderBy(order).ToList(); + } + foreach (var item in self) + { + rv.Add(item); + if (item.Children != null) + { + rv.AddRange(item.GetTreeSelectChildren(order)); + } + } + return rv; + } + + /// + /// 获取TreeSelect节点下所有子节点 + /// + /// + /// + /// + public static List GetTreeSelectChildren(this TreeSelectListItem self, Func order = null) + { + List rv = new List(); + var children = self.Children; + if (order != null && children != null) + { + children = children.OrderBy(order).ToList(); + } + if (children != null && children.Count() > 0) + { + var dictinct = children.Where(x => x.Value != self.Value).ToList(); + foreach (var item in dictinct) + { + rv.Add(item); + //递归添加子节点的子节点 + rv.AddRange(item.GetTreeSelectChildren(order)); + } + } + return rv; + } + + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/ListExtension.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/ListExtension.cs new file mode 100644 index 0000000..51141d4 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/ListExtension.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; + +namespace WalkingTec.Mvvm.Core.Extensions +{ + public static class ListExtension + { + /// + /// 将数据的List转化为下拉菜单数据List + /// + /// 源数据类 + /// 源数据List + /// 指向text字段的表达式 + /// 指向value字段的表达式 + /// 默认被选中的条件 + /// 下拉菜单数据List + public static List ToListItems(this List self + , Expression> textField + , Expression> valueField + , Expression> selectedCondition = null) + { + var rv = new List(); + if (self != null) + { + //循环列表中的数据 + foreach (var item in self) + { + //获取textField的值作为text + string text = textField.Compile().Invoke(item).ToString(); + //获取valueField的值作为value + string value = valueField.Compile().Invoke(item).ToString(); + //添加到下拉菜单List中 + ComboSelectListItem li = new ComboSelectListItem(); + li.Text = text; + li.Value = value; + //如果有默认选择的条件,则将当前数据带入到判断表达式中,如果返回true,则将下拉数据的selected属性设为true + if (selectedCondition != null) + { + if (selectedCondition.Compile().Invoke(item)) + { + li.Selected = true; + } + } + rv.Add(li); + } + } + return rv; + } + + public static object ToChartData(this List self, int radius = 100, string seriesname = "Info") + { + //var data = string.Empty; + if (self != null && self.Count > 0) + { + var cd = self as List; + var i = 0; + for (i = 0; i < cd.Count; i++) + { + if (string.IsNullOrEmpty(cd[i].Series)) + { + cd[i].Series = "Data"; + } + } + string[] series = cd.Select(x => x.Series).Distinct().ToArray(); + + + var yCount = cd.GroupBy(x => x.Category).ToList(); + var isScatter = cd.Any(x => x.ValueX > 0); + var dataset = "{\"source\":["; + if (isScatter) + { + dataset = "[{\"source\":["; + i = 0; + foreach (var item in cd) + { + + dataset += $"[{item.ValueX},{item.Value},{item.Addition},\"{item.Category}\",\"{item.Series}\"]"; + + if (i < cd.Count - 1) + { + dataset += ","; + } + i++; + } + dataset += "]},"; + for (i = 0; i < series.Length; i++) + { + dataset += $"{{\"transform\": {{\"type\": \"filter\",\"config\": {{\"dimension\": 4,\"value\": \"{series[i]}\"}}}}}}"; + if (i < series.Length - 1) + { + dataset += ","; + } + } + dataset += "]"; + } + else + { + object[,] rtc = new object[yCount.Count + 1, series.Length + 1]; + rtc[0, 0] = $"{seriesname}"; + + for (i = 0; i < series.Length; i++) + { + rtc[0, i + 1] = series[i]; + } + i = 0; + foreach (var item in yCount) + { + rtc[i + 1, 0] = item.Key; + for (int j = 0; j < series.Length; j++) + { + var ser = item.Where(x => x.Series == series[j])?.FirstOrDefault(); + if (ser != null) + { + rtc[i + 1, j + 1] = ser.Value; + } + else + { + rtc[i + 1, j + 1] = 0; + } + } + + i++; + } + for (i = 0; i <= yCount.Count; i++) + { + dataset += "["; + for (int j = 0; j <= series.Length; j++) + { + dataset += $"\"{rtc[i, j]}\""; + if (j < series.Length) + { + dataset += ","; + } + } + dataset += "]"; + if (i < yCount.Count) + { + dataset += ","; + } + } + dataset += "]}"; + } + var seriesStr = "["; + var legend = "{\"data\":["; + var max = cd.Max(x => x.Addition); + for (i = 0; i < series.Length; i++) + { + seriesStr += $"{{\"type\":\"charttype\""; + if (isScatter) + { + seriesStr += $",\"encode\":{{\"x\":0,\"y\":1,\"tooltip\":[2,3]}},\"name\": \"{series[i]}\",\"datasetIndex\": {i + 1}"; + seriesStr += $",\"symbolSize\": \"function(data) {{return data[2] / ({max} / {radius})}}\""; + } + seriesStr += "}"; + legend += $"\"{series[i]}\""; + if (i < series.Length - 1) + { + seriesStr += ","; + legend += ","; + } + } + legend += "]}"; + seriesStr += "]"; + return new { dataset = dataset, series = seriesStr, legend = legend }; + } + else + { + var dataset = "{\"source\":[]}"; + var seriesStr = "[]"; + var legend = "{\"data\":[]}"; + return new { dataset = dataset, series = seriesStr, legend = legend }; + } + } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/ListVMExtension.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/ListVMExtension.cs new file mode 100644 index 0000000..694d533 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/ListVMExtension.cs @@ -0,0 +1,377 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace WalkingTec.Mvvm.Core.Extensions +{ + public static class ListVMExtension + { + /// + /// 获取Jason格式的列表数据 + /// + /// 是否需要对数据进行Json编码 + /// 不在后台进行ColumnFormatInfo的转化,而是直接输出ColumnFormatInfo的json结构到前端,由前端处理,默认False + /// + /// Json格式的数据 + public static string GetDataJson(this IBasePagedListVM self, bool returnColumnObject = false, bool enumToString = true) where T : TopBasePoco, new() + { + var sb = new StringBuilder(); + self.GetHeaders(); + if (self.IsSearched == false) + { + self.DoSearch(); + } + var el = self.GetEntityList().ToList(); + //如果列表主键都为0,则生成自增主键,避免主键重复 + if (el.All(x => { + var id = x.GetID(); + if(id == null || (id is Guid gid && gid == Guid.Empty) || (id is int iid && iid==0) || (id is long lid && lid == 0)) + { + return true; + } + else + { + return false; + } + } )) + { + el.ForEach(x => x.ID = Guid.NewGuid()); + } + //循环生成列表数据 + for (int x = 0; x < el.Count; x++) + { + var sou = el[x]; + sb.Append(self.GetSingleDataJson(sou, returnColumnObject, x, enumToString)); + if (x < el.Count - 1) + { + sb.Append(','); + } + } + return $"[{sb}]"; + } + + private static string GetFormatResult(BaseVM vm, ColumnFormatInfo info) + { + string rv = ""; + switch (info.FormatType) + { + case ColumnFormatTypeEnum.Dialog: + rv = vm.UIService.MakeDialogButton(info.ButtonType, info.Url, info.Text, info.Width, info.Height, info.Title, info.ButtonID, info.ShowDialog, info.Resizable, info.Maxed, info.ButtonClass, info.Style).ToString(); + break; + case ColumnFormatTypeEnum.Button: + rv = vm.UIService.MakeButton(info.ButtonType, info.Url, info.Text, info.Width, info.Height, info.Title, info.ButtonID, info.Resizable, info.Maxed, vm.ViewDivId, info.ButtonClass, info.Style, info.RType).ToString(); + break; + case ColumnFormatTypeEnum.Download: + if (info.FileID == null) + { + rv = ""; + } + else + { + rv = vm.UIService.MakeDownloadButton(info.ButtonType, info.FileID.Value, info.Text, vm.CurrentCS, info.ButtonClass, info.Style).ToString(); + } + break; + case ColumnFormatTypeEnum.ViewPic: + if (info.FileID == null) + { + rv = ""; + } + else + { + rv = vm.UIService.MakeViewButton(info.ButtonType, info.FileID.Value, info.Text, info.Width, info.Height, info.Title, info.Resizable, vm.CurrentCS, info.Maxed, info.ButtonClass, info.Style).ToString(); + } + break; + case ColumnFormatTypeEnum.Script: + rv = vm.UIService.MakeScriptButton(info.ButtonType, info.Text, info.Script, info.ButtonID, info.Url, info.ButtonClass, info.Style).ToString(); + break; + case ColumnFormatTypeEnum.Html: + rv = info.Html; + break; + default: + break; + } + return rv; + } + + /// + /// 生成单条数据的Json格式 + /// + /// + /// + /// 数据 + /// 不在后台进行ColumnFormatInfo的转化,而是直接输出ColumnFormatInfo的json结构到前端,由前端处理,默认False + /// index + /// + /// Json格式的数据 + public static string GetSingleDataJson(this IBasePagedListVM self, object obj, bool returnColumnObject, int index = 0, bool enumToString = true) where T : TopBasePoco + { + bool inner = false; + var sb = new StringBuilder(); + var RowBgColor = string.Empty; + var RowColor = string.Empty; + if (obj is not T sou) + { + sou = self.CreateEmptyEntity(); + } + RowBgColor = self.SetFullRowBgColor(sou); + RowColor = self.SetFullRowColor(sou); + var isSelected = self.GetIsSelected(sou); + //循环所有列 + sb.Append('{'); + bool containsID = false; + bool addHiddenID = false; + Dictionary colorcolumns = new Dictionary(); + foreach (var baseCol in self.GetHeaders()) + { + foreach (var col in baseCol.BottomChildren) + { + inner = false; + if (col.ColumnType != GridColumnTypeEnum.Normal) + { + continue; + } + if (col.FieldName?.ToLower() == "id") + { + containsID = true; + } + var backColor = col.GetBackGroundColor(sou); + //获取ListVM中设定的单元格前景色 + var foreColor = col.GetForeGroundColor(sou); + + if (backColor == string.Empty) + { + backColor = RowBgColor; + } + if (foreColor == string.Empty) + { + foreColor = RowColor; + } + (string bgcolor, string forecolor) colors = (null, null); + if (backColor != string.Empty) + { + colors.bgcolor = backColor; + } + if (foreColor != string.Empty) + { + colors.forecolor = foreColor; + } + if (string.IsNullOrEmpty(colors.bgcolor) == false || string.IsNullOrEmpty(colors.forecolor) == false) + { + colorcolumns.Add(col.Field, colors); + } + //设定列名,如果是主键ID,则列名为id,如果不是主键列,则使用f0,f1,f2...这种方式命名,避免重复 + var ptype = col.FieldType; + if (col.Field?.ToLower() == "children" && typeof(IEnumerable).IsAssignableFrom(ptype)) + { + var children = ((IEnumerable)col.GetObject(obj))?.ToList(); + if (children == null || children.Count == 0) + { + continue; + } + } + var html = string.Empty; + + if (col.EditType == EditTypeEnum.Text || col.EditType == null) + { + if (typeof(IEnumerable).IsAssignableFrom(ptype)) + { + var children = ((IEnumerable)col.GetObject(obj))?.ToList(); + if (children != null) + { + html = "["; + for (int i = 0; i < children.Count; i++) + { + var item = children[i]; + html += self.GetSingleDataJson(item, returnColumnObject,0,enumToString); + if (i < children.Count - 1) + { + html += ","; + } + } + html += "]"; + } + else + { + //html = "[]"; + } + inner = true; + } + else + { + if (returnColumnObject == true) + { + html = col.GetText(sou, false).ToString(); + } + else + { + var info = col.GetText(sou); + + if (info is ColumnFormatInfo) + { + html = GetFormatResult(self as BaseVM, info as ColumnFormatInfo); + } + else if (info is List) + { + var temp = string.Empty; + foreach (var item in info as List) + { + temp += GetFormatResult(self as BaseVM, item); + temp += "  "; + } + html = temp; + } + else + { + html = info.ToString(); + } + } + + //如果列是布尔值,直接返回true或false,让前台生成CheckBox + if (ptype == typeof(bool) || ptype == typeof(bool?)) + { + if(enumToString == false) + { + html = html.ToLower(); + inner = true; + } + else if (returnColumnObject == false) + { + if (html.ToLower() == "true") + { + html = (self as BaseVM).UIService.MakeCheckBox(true, isReadOnly: true); + } + if (html.ToLower() == "false" || html == string.Empty) + { + html = (self as BaseVM).UIService.MakeCheckBox(false, isReadOnly: true); + } + } + else + { + if (html != null && html != string.Empty) + { + html = html.ToLower(); + } + } + } + //如果列是枚举,直接使用枚举的文本作为多语言的Key查询多语言文字 + else if (ptype.IsEnumOrNullableEnum()) + { + if (enumToString == true) + { + string enumdisplay = PropertyHelper.GetEnumDisplayName(ptype, html); + if (string.IsNullOrEmpty(enumdisplay) == false) + { + html = enumdisplay; + } + } + } + //If this column is a class or list, html will be set to a json string, sest inner to true to remove the " + if (returnColumnObject == true && ptype?.Namespace.Equals("System") == false && ptype?.IsEnumOrNullableEnum() == false) + { + inner = true; + } + } + if (enumToString == false && string.IsNullOrEmpty(html)) + { + continue; + } + + } + else + { + string val = col.GetText(sou).ToString(); + string name = $"{self.DetailGridPrix}[{index}].{col.Field}"; + switch (col.EditType) + { + case EditTypeEnum.TextBox: + html = (self as BaseVM).UIService.MakeTextBox(name, val); + break; + case EditTypeEnum.CheckBox: + _ = bool.TryParse(val, out bool nb); + html = (self as BaseVM).UIService.MakeCheckBox(nb, null, name, "true"); + break; + case EditTypeEnum.ComboBox: + html = (self as BaseVM).UIService.MakeCombo(name, col.ListItems, val); + break; + case EditTypeEnum.Datetime: + html = (self as BaseVM).UIService.MakeDateTime(name, val); + break; + default: + break; + } + } + if (string.IsNullOrEmpty(self.DetailGridPrix) == false && addHiddenID == false) + { + html += $@""; + addHiddenID = true; + } + if (inner == false) + { + html = "\"" + html.Replace(Environment.NewLine, "").Replace("\t", string.Empty).Replace("\n", string.Empty).Replace("\r", string.Empty).Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; + } + sb.Append($"\"{col.Field}\":"); + sb.Append(html); + sb.Append(','); + } + } + sb.Append($"\"TempIsSelected\":\"{ (isSelected == true ? "1" : "0") }\""); + foreach (var cc in colorcolumns) + { + if (string.IsNullOrEmpty(cc.Value.Item1) == false) + { + string bg = cc.Value.Item1; + if (bg.StartsWith("#") == false) + { + bg = "#" + bg; + } + sb.Append($",\"{cc.Key}__bgcolor\":\"{bg}\""); + } + if (string.IsNullOrEmpty(cc.Value.Item2) == false) + { + string fore = cc.Value.Item2; + if (fore.StartsWith("#") == false) + { + fore = "#" + fore; + } + sb.Append($",\"{cc.Key}__forecolor\":\"{fore}\""); + } + } + if (containsID == false) + { + sb.Append($",\"ID\":\"{(sou as dynamic).ID}\""); + } + // 标识当前行数据是否被选中 + sb.Append($@",""LAY_CHECKED"":{sou.Checked.ToString().ToLower()}"); + sb.Append(string.Empty); + sb.Append('}'); + return sb.ToString(); + } + + /// + /// Get json format string of ListVM's search result + /// + /// Model type + /// a listvm + /// true to return plain text, false to return formated html, such as checkbox,buttons ... + /// use enum display name + /// json string + public static string GetJson(this IBasePagedListVM self, bool PlainText = true, bool enumToString = true) where T : TopBasePoco, new() + { + return $@"{{""Data"":{self.GetDataJson(PlainText,enumToString)},""Count"":{self.Searcher.Count},""Page"":{self.Searcher.Page},""PageCount"":{self.Searcher.PageCount},""Msg"":""success"",""Code"":200}}"; + } + + public static object GetJsonForApi(this IBasePagedListVM self, bool PlainText = true) where T : TopBasePoco, new() + { + return new { Data = self.GetEntityList(), Count = self.Searcher.Count, PageCount = self.Searcher.PageCount, Page = self.Searcher.Page, Msg = "success", Code = 200 }; + } + + + public static string GetError(this IBasePagedListVM self) where T : TopBasePoco, new() + { + return $@"{{""Data"":{{}},""Count"":0,""Page"":0,""PageCount"":0,""Msg"":""{(self as BaseVM).MSD.GetFirstError()}"",""Code"":400}}"; + } + + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/LoginUserInfoExtension.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/LoginUserInfoExtension.cs new file mode 100644 index 0000000..0795d61 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/LoginUserInfoExtension.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; + +using Microsoft.Extensions.Caching.Distributed; + +using WalkingTec.Mvvm.Core.Auth; + +namespace WalkingTec.Mvvm.Core.Extensions +{ + public static class LoginUserInfoExtension + { + public static ClaimsPrincipal CreatePrincipal(this LoginUserInfo self) + { + if (string.IsNullOrEmpty(self.ITCode)) throw new ArgumentException("Id is mandatory", nameof(self.ITCode)); + var claims = new List { new Claim(AuthConstants.JwtClaimTypes.Subject, self.ITCode) }; + + if (!string.IsNullOrEmpty(self.Name)) + { + claims.Add(new Claim(AuthConstants.JwtClaimTypes.Name, self.Name)); + } + if (!string.IsNullOrEmpty(self.TenantCode)) + { + claims.Add(new Claim(AuthConstants.JwtClaimTypes.TenantCode, self.TenantCode)); + } + + var id = new ClaimsIdentity( + claims.Distinct(new ClaimComparer()), + AuthConstants.AuthenticationType, + AuthConstants.JwtClaimTypes.Name, + AuthConstants.JwtClaimTypes.TenantCode); + return new ClaimsPrincipal(id); + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/PagedListExtension.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/PagedListExtension.cs new file mode 100644 index 0000000..28cb96a --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/PagedListExtension.cs @@ -0,0 +1,323 @@ +using System; +using System.Linq.Expressions; + +namespace WalkingTec.Mvvm.Core.Extensions +{ + public static class PagedListExtension + { + #region MakeGridColumn 生成GridColumn + /// + /// 生成GridColumn + /// + /// 继承自TopBasePoco的类 + /// 继承自ISearcher的类 + /// self + /// 指向T中字段的表达式 + /// 格式化显示内容的委托函数,函数接受两个参数,第一个是整行数据,第二个是所选列的数据 + /// 表头名称 + /// 列宽 + /// 是否填充 + /// 是否允许多行 + /// 是否需要分组 + /// 设置前景色的委托函数 + /// 设置背景色的委托函数 + /// 返回设置好的GridColumn类的实例 + public static GridColumn MakeGridColumn(this IBasePagedListVM self + , Expression> ColumnExp + , ColumnFormatCallBack Format = null + , string Header = null + , int? Width = null + , int? Flex = null + , bool AllowMultiLine = true + , bool NeedGroup = false + , Func ForeGroundFunc = null + , Func BackGroundFunc = null) + where T : TopBasePoco + where V : ISearcher + { + GridColumn rv = new GridColumn(ColumnExp, Format, Header, Width, Flex, AllowMultiLine, NeedGroup, ForeGroundFunc, BackGroundFunc); + return rv; + } + #endregion + + #region MakeActionGridColumn 生成Grid动作列(增删该查) + ///// + ///// 生成Grid动作列(增删该查) + ///// + ///// 继承自TopBasePoco的类 + ///// 继承自ISearcher的类 + ///// self + ///// 表头名称 + ///// 列宽 + ///// 返回设置好的动作列 + //public static GridColumn MakeActionGridColumn(this IBasePagedListVM self + // , string Header = null + // , int? Width = null) + // where T : TopBasePoco + // where V : ISearcher + //{ + // ActionGridColumn rv = new ActionGridColumn(Header, Width); + // rv.Sortable = false; + // return rv; + //} + #endregion + + #region MakeGridAction 创建一个新的列表动作 + ///// + ///// 创建一个新的列表动作 + ///// + ///// 继承自TopBasePoco的类 + ///// 继承自ISearcher的类 + ///// self + ///// 动作名 + ///// 弹出窗口标题 + ///// 动作的Controller + ///// 动作的Action + ///// 动作类型 + ///// 是否在每行都显示 + ///// 域名 + ///// 是否需要弹出窗口,默认为true + ///// 弹出窗口的宽度 + ///// 弹出窗口的高度 + ///// 动作图标css,默认为null,没有图标 + ///// 是否在工具栏隐藏 默认false + ///// Button的id 默认自动生成 + ///// 是否在新页面打开 + ///// 列表动作 + //public static GridAction MakeGridAction(this IBasePagedListVM self + // , string name + // , string dialogTitle + // , string controllerName + // , string actionName + // , GridActionParameterTypesEnum paraType + // , bool showInRow + // , string areaName = null + // , bool showDialog = true + // , int? dialogWidth = null + // , int? dialogHeight = null + // , string iconCls = null + // , bool hideOnToolBar = false + // , string buttonId = null + // , bool isRedirect = false + // , bool resizable = true) + // where T : TopBasePoco + // where V : ISearcher + //{ + // return new GridAction { Name = name, Area = areaName, ControllerName = controllerName, DialogTitle = dialogTitle, ActionName = actionName, DialogWidth = dialogWidth, DialogHeight = dialogHeight, ParameterType = paraType, ShowInRow = showInRow, OnClickFunc = null, ShowDialog = showDialog, IconCls = iconCls, HideOnToolBar = hideOnToolBar, ButtonId = buttonId, IsRedirect = isRedirect, Resizable = resizable }; + //} + #endregion + + #region MakeScriptGridAction 创建一个执行自定义js的列表动作 + ///// + ///// 创建一个执行自定义js的列表动作 + ///// + ///// 继承自TopBasePoco的类 + ///// 继承自ISearcher的类 + ///// self + ///// 动作的Controller + ///// 动作的Action + ///// 动作名 + ///// 点击动作按钮执行的js + ///// 是否在操作列中显示 默认true + ///// 是否在工具栏中隐藏 默认true + ///// 动作的区域 + ///// Button的id 默认自动生成 + ///// 列表动作 + ///// + ///// 框架的自动权限验证无法验证自定义的js中跳转到的链接, + ///// 所以虽然只是运行js,但如果js最终跳转到某个链接,则还是需要在这里指定Controller和Action,这样框架就可以自动判断权限 + ///// + //public static GridAction MakeScriptGridAction(this IBasePagedListVM self + // , string controllerName + // , string actionName + // , string name + // , string script + // , string areaName = null + // , string buttonId = null + // , bool showInRow = true + // , bool hideOnToolBar = true + // , bool resizable = true)//add by wuwh 2014.05.07 添加hideOnToolBar参数 + // where T : TopBasePoco + // where V : ISearcher + //{ + // return new GridAction { Area = areaName, ControllerName = controllerName, ActionName = actionName, Name = name, OnClickFunc = script, ShowInRow = showInRow, ButtonId = buttonId, HideOnToolBar = hideOnToolBar, Resizable = resizable }; + //} + #endregion + + #region MakeStandardAction 创建标准动作 + ////新增参数 whereStr + ///// + ///// 创建标准动作 + ///// + ///// 继承自TopBasePoco的类 + ///// 继承自ISearcher的类 + ///// self + ///// 动作的Controller + ///// 标准动作类型 + ///// 弹出窗口的标题 + ///// 域名 + ///// 弹出窗口的宽度 + ///// 弹出窗口的高度 + ///// 动作名,默认为‘新建’ + ///// Button的id 默认自动生成 + ///// 列表动作 + ///// + ///// 根据标准动作类型,创建默认属性的标准动作 + ///// + //public static GridAction MakeStandardAction(this IBasePagedListVM self + // , string controllerName + // , GridActionStandardTypesEnum standardType + // , string dialogTitle + // , string areaName = null + // , int? dialogWidth = null + // , int? dialogHeight = null + // , string name = null + // , string buttonId = null + // , bool resizable = true + // , params Expression>[] whereStr) + // where T : TopBasePoco + // where V : ISearcher + //{ + // string iconcls = ""; + // string action = ""; + // string gridname = ""; + // GridActionParameterTypesEnum paraType = GridActionParameterTypesEnum.NoId; + // bool showinrow = false; + // bool hideontoolbar = false; + // switch (standardType) + // { + // case GridActionStandardTypesEnum.Create: + // iconcls = "icon-add"; + // action = "Create"; + // gridname = "新建"; + // paraType = GridActionParameterTypesEnum.NoId; + // break; + // case GridActionStandardTypesEnum.Edit: + // iconcls = "icon-edit"; + // action = "Edit"; + // gridname = "修改"; + // paraType = GridActionParameterTypesEnum.SingleId; + // showinrow = true; + // hideontoolbar = true; + // break; + // case GridActionStandardTypesEnum.Delete: + // iconcls = "icon-delete"; + // action = "Delete"; + // gridname = "删除"; + // paraType = GridActionParameterTypesEnum.SingleId; + // showinrow = true; + // hideontoolbar = true; + // break; + // case GridActionStandardTypesEnum.Details: + // iconcls = "icon-details"; + // action = "Details"; + // gridname = "详细"; + // paraType = GridActionParameterTypesEnum.SingleId; + // showinrow = true; + // hideontoolbar = true; + // break; + // case GridActionStandardTypesEnum.BatchEdit: + // iconcls = "icon-edit"; + // action = "BatchEdit"; + // gridname = "批量修改"; + // paraType = GridActionParameterTypesEnum.MultiIds; + // break; + // case GridActionStandardTypesEnum.BatchDelete: + // iconcls = "icon-delete"; + // action = "BatchDelete"; + // gridname = "批量删除"; + // paraType = GridActionParameterTypesEnum.MultiIds; + // break; + // case GridActionStandardTypesEnum.Import: + // iconcls = "icon-details"; + // action = "ImportExcelData"; + // gridname = "导入"; + // paraType = GridActionParameterTypesEnum.NoId; + // break; + // default: + // break; + // } + // List list = new List(); + // foreach (var item in whereStr) + // { + // list.Add(PropertyHelper.GetPropertyName(item)); + // } + + // return new GridAction { Name = (name == null ? gridname : name), DialogTitle = dialogTitle, IconCls = iconcls, ControllerName = controllerName, ActionName = action, Area = areaName, DialogWidth = dialogWidth, DialogHeight = dialogHeight, ParameterType = paraType, ShowInRow = showinrow, ShowDialog = true, HideOnToolBar = hideontoolbar, ButtonId = buttonId, Resizable = resizable, whereStr = list.ToArray() }; + //} + #endregion + + #region MakeStandardExportAction 创建标准导出按钮 + ///// + ///// 创建标准导出按钮 + ///// + ///// 继承自TopBasePoco的类 + ///// 继承自ISearcher的类 + ///// self + ///// vmGuid + ///// + ///// 导出类型 默认null,支持所有导出 + ///// 参数 + ///// + //public static GridAction MakeStandardExportAction(this IBasePagedListVM self + // , string gridid = null + // , bool MustSelect = false + // , ExportEnum? exportType = null + // , params KeyValuePair[] param) + // where T : TopBasePoco + // where V : ISearcher + //{ + // string excelscript = ""; + // string pdfscript = ""; + + // string parameters = "?"; + // foreach (var item in param) + // { + // parameters += item.Key + "=" + item.Value + "&"; + // } + // if (self.FromFixedCon == true) + // { + // parameters += "DONOTUSECSName=" + self.CurrentCS + "&"; + // } + // parameters += "1=1"; + // if (gridid == null) + // { + // gridid = "Grid" + self.UniqueId; + // } + // if (MustSelect == false) + // { + // excelscript = string.Format("FF_ShowMask(\"{1}\");FF_DownloadExcelOrPdfPost(\"{0}\",\"/WebApi/Home/ExportExcel{2}\");", gridid, "正在生成文件...", parameters); + // pdfscript = string.Format("FF_ShowMask(\"{1}\");FF_DownloadExcelOrPdfPost(\"{0}\",\"/WebApi/Home/ExportPdf{2}\");", gridid, "正在生成文件...", parameters); + // } + // else + // { + // excelscript += "var sels = Ext.getCmp(\"" + gridid + "\").getSelectionModel().getSelection();"; + // excelscript += "if(sels.length == 0){ FF_OpenSimpleDialog(\"" + "错误" + "\",\"" + "请至少选择一条数据" + "\");} else{"; + // excelscript += string.Format("FF_ShowMask(\"{1}\");FF_DownloadExcelOrPdfPost(\"{0}\",\"/WebApi/Home/ExportExcel{2}\");", gridid, "正在生成文件...", parameters) + "}"; + + // pdfscript += "var sels = Ext.getCmp(\"" + gridid + "\").getSelectionModel().getSelection();"; + // pdfscript += "if(sels.length == 0){ FF_OpenSimpleDialog(\"" + "错误" + "\",\"" + "请至少选择一条数据" + "\");} else{"; + // pdfscript += string.Format("FF_ShowMask(\"{1}\");FF_DownloadExcelOrPdfPost(\"{0}\",\"/WebApi/Home/ExportPdf{2}\");", gridid, "正在生成文件...", parameters) + "}"; + // } + // GridAction ga = null; + // if (exportType == ExportEnum.Excel) + // { + // ga = new GridAction { Area = "WebApi", ControllerName = "Home", ActionName = "ExportExcel", Name = "导出Excel", ParameterType = GridActionParameterTypesEnum.NoId, OnClickFunc = excelscript, ShowInRow = false, Resizable = true }; + // } + // else if (exportType == ExportEnum.PDF) + // { + // ga = new GridAction { Area = "WebApi", ControllerName = "Home", ActionName = "ExportExcel", Name = "导出PDF", ParameterType = GridActionParameterTypesEnum.NoId, OnClickFunc = excelscript, ShowInRow = false, Resizable = true }; + // } + // else + // { + // ga = new GridAction { Area = "WebApi", ControllerName = "Home", ActionName = "ExportExcel", Name = "导出", ParameterType = GridActionParameterTypesEnum.NoId, OnClickFunc = excelscript, ShowInRow = false, Resizable = true }; + // GridAction excel = new GridAction { Area = "WebApi", ControllerName = "Home", ActionName = "ExportExcel", Name = "导出Excel", ParameterType = GridActionParameterTypesEnum.NoId, OnClickFunc = excelscript, ShowInRow = false, Resizable = true }; + // GridAction pdf = new GridAction { Area = "WebApi", ControllerName = "Home", ActionName = "ExportPdf", Name = "导出PDF", ParameterType = GridActionParameterTypesEnum.NoId, OnClickFunc = pdfscript, ShowInRow = false, Resizable = true }; + // ga.SubActions = new List { excel, pdf }; + // } + // return ga; + //} + #endregion + + } +} \ No newline at end of file diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/SystemExtensions/DateTimeHelper.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/SystemExtensions/DateTimeHelper.cs new file mode 100644 index 0000000..584e350 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/SystemExtensions/DateTimeHelper.cs @@ -0,0 +1,81 @@ +using System; + +namespace WalkingTec.Mvvm.Core.Extensions +{ + /// + /// DateTime Helper + /// + public static class DateTimeHelper + { + #region DateTime Helper + + public static int WeekOfYear(this DateTime self) + { + var startDayOfYear = new DateTime(self.Year,1,1); + var weekOffset = 7 - ( startDayOfYear.DayOfWeek == DayOfWeek.Sunday ? 7 : (int)startDayOfYear.DayOfWeek) + 1; + var weekOfYear = (int)Math.Ceiling((self.DayOfYear - weekOffset) / 7.0 + (weekOffset == 0 ? 0 : 1)); + + return weekOfYear; + } + + /// + /// 获取 指定的一周所在年份 的开始及结束时间 + /// + /// 所在年份 + /// 周数 + /// 指定周开始时间 + /// 指定周结束时间 + public static void WeekDays(int yearNum, int weekOfYear, out DateTime startDay, out DateTime endDay) + { + var startDayOfYear = new DateTime(yearNum,1,1,0,0,0); + + var weekOffset = 7 - ( startDayOfYear.DayOfWeek == DayOfWeek.Sunday ? 7 : (int)startDayOfYear.DayOfWeek) + 1; + startDay = startDayOfYear.AddDays(7 * (weekOfYear - (weekOffset == 0 ? 0 : 1)) + weekOffset - 7); + endDay = startDay.AddDays(7); + } + + #endregion + + #region DateTime Extensions + + private static readonly DateTime _jan1st1970 = new DateTime(1970, 1, 1, 0, 0, 0,DateTimeKind.Utc); + + /// + /// UTC 1970/01/01 00:00:00 + /// + public static DateTime Jan1st1970 => _jan1st1970; + + /// + /// 时间戳 ms + /// + /// + /// 返回标准时间戳 单位 毫秒 注:从 1970/01/01 00:00:00 开始 + public static long ToMilliseconds(this DateTime self) + { + return (long)(self.ToUniversalTime() - Jan1st1970).TotalMilliseconds; + } + + /// + /// 时间戳 microsecond + /// + /// + /// 返回标准时间戳 单位 微秒 注:从 1970/01/01 00:00:00 开始 + public static long ToMicroseconds(this DateTime self) + { + return (long)((self.ToUniversalTime() - Jan1st1970).TotalMilliseconds * 1000); + } + + /// + /// 获取当前时间所在周的开始及结束时间 + /// + /// + /// 指定周开始时间 + /// 指定周结束时间 + public static void WeekDays(this DateTime self, out DateTime startDay, out DateTime endDay) + { + WeekDays(self.Year, self.WeekOfYear(), out startDay, out endDay); + } + + #endregion + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/SystemExtensions/DistributedCacheExtensions.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/SystemExtensions/DistributedCacheExtensions.cs new file mode 100644 index 0000000..40a6093 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/SystemExtensions/DistributedCacheExtensions.cs @@ -0,0 +1,158 @@ +// +// DistributedCacheExtensions.cs +// +// Author: +// Michael,Vito +// +// Copyright (c) 2019 WTM +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System.Reflection; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Caching.Distributed; + + +namespace WalkingTec.Mvvm.Core.Extensions +{ + public static class DistributedCacheExtensions + { + /// + /// CachingKey 分割符 + /// + private const string SPLIT_CHAR = ":"; + + private static string _instanceName; + private static string InstanceName + { + get + { + if (_instanceName == null) + { + _instanceName = Assembly.GetEntryAssembly().GetName().Name + SPLIT_CHAR; + } + return _instanceName; + } + } + + public static void SetInstanceName( + this IDistributedCache cache, + string instanceName) + { + _instanceName = instanceName + SPLIT_CHAR; + } + + #region Get + + public static T Get( + this IDistributedCache cache, + string key) + { + var value = cache.GetString(InstanceName + key.ToLower()); + if (value == null) + return default; + else + return JsonSerializer.Deserialize(value,Core.CoreProgram.DefaultJsonOption); + } + + public static async Task GetAsync( + this IDistributedCache cache, + string key, + CancellationToken token = default) + { + var value = await cache.GetStringAsync(InstanceName + key.ToLower(), token); + if (value == null) + return default; + else + return JsonSerializer.Deserialize(value, Core.CoreProgram.DefaultJsonOption); + } + + public static bool TryGetValue( + this IDistributedCache cache, + string key, + out T outValue) + { + var value = cache.GetString(InstanceName + key.ToLower()); + if (value == null) + { + outValue = default; + return false; + } + else + { + outValue = JsonSerializer.Deserialize(value, Core.CoreProgram.DefaultJsonOption); + return true; + } + } + + #endregion + + #region Set + + public static void Add( + this IDistributedCache cache, + string key, + T value, + DistributedCacheEntryOptions options = null) + { + if (options == null) + cache.Set(InstanceName + key.ToLower(), Encoding.UTF8.GetBytes(JsonSerializer.Serialize(value, CoreProgram.DefaultJsonOption))); + else + cache.Set(InstanceName + key.ToLower(), Encoding.UTF8.GetBytes(JsonSerializer.Serialize(value, CoreProgram.DefaultJsonOption)), options); + } + + public static async Task AddAsync( + this IDistributedCache cache, + string key, + T value, + DistributedCacheEntryOptions options = null, + CancellationToken token = default) + { + if (options == null) + await cache.SetAsync(InstanceName + key.ToLower(), Encoding.UTF8.GetBytes(JsonSerializer.Serialize(value, CoreProgram.DefaultJsonOption)), token); + else + await cache.SetAsync(InstanceName + key.ToLower(), Encoding.UTF8.GetBytes(JsonSerializer.Serialize(value, CoreProgram.DefaultJsonOption)), options, token); + } + + #endregion + + + #region Delete + + public static void Delete( + this IDistributedCache cache, + string key) + { + cache.Remove(InstanceName + key.ToLower()); + } + + public static async Task DeleteAsync( + this IDistributedCache cache, + string key, + CancellationToken token = default) + { + await cache.RemoveAsync(InstanceName + key.ToLower(), token); + } + + #endregion + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/SystemExtensions/EnumExtension.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/SystemExtensions/EnumExtension.cs new file mode 100644 index 0000000..a28cde8 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/SystemExtensions/EnumExtension.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace WalkingTec.Mvvm.Core.Extensions +{ + /// + /// 枚举扩展函数 + /// + public static class EnumExtension + { + #region 将枚举类型转化为下拉列表 + /// + /// 将枚举类型转化为下拉列表 + /// + /// 枚举类型 + /// value + /// pleaseSelect + /// 下拉菜单数据列表 + public static List ToListItems(this Type self, object value = null, bool pleaseSelect = false) + { + string[] names = null; + Array values = null; + //如果是枚举 + if (self.IsEnum) + { + names = Enum.GetNames(self); + values = Enum.GetValues(self); + } + //如果是nullable的枚举 + if (self.IsGenericType && self.GenericTypeArguments[0].IsEnum) + { + names = Enum.GetNames(self.GenericTypeArguments[0]); + values = Enum.GetValues(self.GenericTypeArguments[0]); + } + //生成下拉菜单数据 + List rv = new List(); + if (names != null) + { + for(int i=0;i + /// 字符串辅助类 + /// + public static class StringExtension + { + /// + /// 根据名字获取Id形式 + /// + /// 名字 + /// 将[].转换成_形式的Id + public static string GetIdByName(this string fieldName) + { + return fieldName == null ? "" : fieldName.Replace(".", "_").Replace("[", "_").Replace("]", "_"); + } + + /// + /// 格式化URL + /// + /// 初始url + /// 格式化后的url + public static string CorrectUrl(this string url) + { + if (string.IsNullOrWhiteSpace(url) == true) + { + url = ""; + } + else + { + url = url.ToLower(); + url = url.Trim('/', '\\'); + if (url.StartsWith("http://") == false && url.StartsWith("https://") == false) + { + url = "http://" + url; + } + } + return url; + } + /// + /// 将数据列表转化为逗号分隔的字符串 + /// + /// 源数据类 + /// 文本字段 + /// 源数据List + /// 要拼接的文本字段 + /// 转化文本字段的表达式 + /// 分隔符,默认为逗号 + /// 转化后的字符串 + public static string ToSepratedString(this IEnumerable self, Expression> textField, Func Format = null, string seperator = ",") + { + string rv = ""; + if (self == null) + { + return rv; + } + //循环所有数据 + for (int i = 0; i < self.Count(); i++) + { + //获取文本字段的值 + V text = textField.Compile().Invoke(self.ElementAt(i)); + string str = ""; + //如果有转换函数,则调用获取转换后的字符串 + if (Format == null) + { + if (text == null) + { + str = ""; + } + else + { + str = text.ToString(); + } + } + else + { + str = Format.Invoke(text); + } + rv += str; + //拼接分隔符 + if (i < self.Count() - 1) + { + rv += seperator; + } + } + //返回转化后的字符串 + return rv; + } + + public static string ToSepratedString(this IEnumerable self, Func Format = null, string seperator = ",") + { + string rv = ""; + if (self == null) + { + return rv; + } + foreach (var item in self) + { + var s = ""; + if (Format == null) + { + s = item.ToString(); + } + else + { + s = Format.Invoke(item); + } + if(string.IsNullOrEmpty(s) == false) + { + rv += s + seperator; + } + } + if (rv.Length > 0) + { + rv = rv.Substring(0, rv.Length - 1); + } + return rv; + } + + public static string ToSepratedString(this NameValueCollection self, string seperator = ",") + { + string rv = ""; + if (self == null) + { + return rv; + } + foreach (var item in self) + { + rv += item.ToString() + "=" + self[item.ToString()] + seperator; + } + if (rv.Length > 0) + { + rv = rv.Substring(0, rv.Length - 1); + } + return rv; + } + + public static string AppendQuery(this string self,string query) + { + if(self == null) + { + return null; + } + if (self.Contains("?")) + { + self += "&" + query; + } + else + { + self += "?" + query; + } + return self; + } + + public static string AppendQuery(this string self, IDictionary data) + { + if (self == null) + { + return null; + } + string query = ""; + foreach (IDictionaryEnumerator item in data) + { + query += item.Key + "=" + item.Value + "&"; + } + + if (self.Contains("?")) + { + self += "&" + query; + } + else + { + self += "?" + query; + } + return self; + } + + public static string AppendQuery(this string self, List> data) + { + if (self == null) + { + return null; + } + string query = ""; + foreach (var item in data) + { + query += item.Key + "=" + item.Value + "&"; + } + + if (self.Contains("?")) + { + self += "&" + query; + } + else + { + self += "?" + query; + } + return self; + } + + public static string ToQueryString(this IEnumerable self, string name=null) + { + if(self == null) + { + return ""; + } + if (string.IsNullOrEmpty(name)) + { + name = "id"; + } + string rv = ""; + foreach (var item in self) + { + rv += $"{name}={item?.ToString()}&"; + } + if(rv.Length > 0) + { + rv = rv[0..^1]; + } + return rv; + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/SystemExtensions/SystemExtension.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/SystemExtensions/SystemExtension.cs new file mode 100644 index 0000000..d38ce8a --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/SystemExtensions/SystemExtension.cs @@ -0,0 +1,67 @@ +using System; +using System.Reflection; + +namespace WalkingTec.Mvvm.Core.Extensions +{ + /// + /// System Extension + /// + public static class SystemExtension + { + #region Guid Extensions + + public static string ToNoSplitString(this Guid self) + { + return self.ToString().Replace("-", string.Empty); + } + + #endregion + + /// + /// 将CrudVM中Entity的关联字段设为空并返回一个新的CrudVM + /// + /// + /// + public static object GetCleanCrudVM(this object self) + { + var mtype = self.GetType(); + if(typeof(IBaseCRUDVM).IsAssignableFrom(mtype)) + { + var rv = mtype.GetConstructor(Type.EmptyTypes).Invoke(null); + var toppros = mtype.GetAllProperties(); + foreach (var tpro in toppros) + { + if(tpro.Name == "Entity") + { + var entity = tpro.GetValue(self); + var pros = tpro.PropertyType.GetAllProperties(); + var newEntity = tpro.PropertyType.GetConstructor(Type.EmptyTypes).Invoke(null); + bool isBasePoco = typeof(IBasePoco).IsAssignableFrom(tpro.PropertyType); + //将所有TopBasePoco的属性赋空值,防止添加关联的重复内容 + foreach (var pro in pros) + { + if (pro.PropertyType.GetTypeInfo().IsSubclassOf(typeof(TopBasePoco)) == false) + { + if (isBasePoco == false || (pro.Name != "UpdateTime" && pro.Name != "UpdateBy")) + { + pro.SetValue(newEntity, pro.GetValue(entity)); + } + } + } + tpro.SetValue(rv, newEntity); + } + else + { + if (tpro.CanWrite) + { + tpro.SetValue(rv, tpro.GetValue(self)); + } + } + } + return rv; + } + return null; + } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/SystemExtensions/TypeExtension.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/SystemExtensions/TypeExtension.cs new file mode 100644 index 0000000..71d092e --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Extensions/SystemExtensions/TypeExtension.cs @@ -0,0 +1,527 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Reflection; +using Fare; + +namespace WalkingTec.Mvvm.Core.Extensions +{ + /// + /// Type的扩展函数 + /// + public static class TypeExtension + { + public static ImmutableDictionary> _propertyCache { get; set; } = new Dictionary>().ToImmutableDictionary(); + /// + /// 判断是否是泛型 + /// + /// Type类 + /// 泛型类型 + /// 判断结果 + public static bool IsGeneric(this Type self, Type innerType) + { + if (self.GetTypeInfo().IsGenericType && self.GetGenericTypeDefinition() == innerType) + { + return true; + } + else + { + return false; + } + } + + /// + /// 判断是否为Nullable<>类型 + /// + /// Type类 + /// 判断结果 + public static bool IsNullable(this Type self) + { + return self.IsGeneric(typeof(Nullable<>)); + } + + /// + /// 判断是否为List<>类型 + /// + /// Type类 + /// 判断结果 + public static bool IsList(this Type self) + { + return self.IsGeneric(typeof(List<>)) || self.IsGeneric(typeof(IEnumerable<>)); + } + + /// + /// 判断是否为List<>类型 + /// + /// Type类 + /// 判断结果 + public static bool IsListOf(this Type self) + { + if (self.IsGeneric(typeof(List<>)) && typeof(T).IsAssignableFrom(self.GenericTypeArguments[0])) + { + return true; + } + else + { + return false; + } + } + + + #region 判断是否为枚举 + + /// + /// 判断是否为枚举 + /// + /// Type类 + /// 判断结果 + public static bool IsEnum(this Type self) + { + return self.GetTypeInfo().IsEnum; + } + + /// + /// 判断是否为枚举或者可空枚举 + /// + /// + /// + public static bool IsEnumOrNullableEnum(this Type self) + { + if (self == null) + { + return false; + } + if (self.IsEnum) + { + return true; + } + else + { + if (self.IsGenericType && self.GetGenericTypeDefinition() == typeof(Nullable<>) && self.GetGenericArguments()[0].IsEnum) + { + return true; + } + else + { + return false; + } + } + } + + #endregion + + /// + /// 判断是否为值类型 + /// + /// Type类 + /// 判断结果 + public static bool IsPrimitive(this Type self) + { + return self.GetTypeInfo().IsPrimitive || self == typeof(decimal); + } + + public static bool IsNumber(this Type self) + { + Type checktype = self; + if (self.IsNullable()) + { + checktype = self.GetGenericArguments()[0]; + } + if (checktype == typeof(int) || checktype == typeof(short) || checktype == typeof(long) || checktype == typeof(float) || checktype == typeof(decimal) || checktype == typeof(double)) + { + return true; + } + else + { + return false; + } + } + + + #region 判断是否是Bool + + public static bool IsBool(this Type self) + { + return self == typeof(bool); + } + + /// + /// 判断是否是 bool or bool?类型 + /// + /// + /// + public static bool IsBoolOrNullableBool(this Type self) + { + if (self == null) + { + return false; + } + if (self == typeof(bool) || self == typeof(bool?)) + { + return true; + } + else + { + return false; + } + } + + #endregion + + public static Dictionary GetRandomValues(this Type self) + { + Dictionary rv = new Dictionary(); + string pat = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"; + var pros = self.GetAllProperties(); + List skipFields = new List() + { + nameof(TopBasePoco.BatchError), + nameof(TopBasePoco.Checked), + nameof(TopBasePoco.ExcelIndex), + }; + if (typeof(IBasePoco).IsAssignableFrom(self)) + { + skipFields.AddRange( + new string[]{ + nameof(IBasePoco.CreateBy), + nameof(IBasePoco.CreateTime), + nameof(IBasePoco.UpdateBy), + nameof(IBasePoco.UpdateTime) } + ); + } + if (typeof(IPersistPoco).IsAssignableFrom(self)) + { + skipFields.Add(nameof(IPersistPoco.IsValid)); + } + foreach (var pro in pros) + { + string key = pro.Name; + string val = ""; + var notmapped = pro.GetCustomAttribute(); + if (notmapped == null && + pro.PropertyType.IsList() == false && + pro.PropertyType.IsSubclassOf(typeof(TopBasePoco)) == false && + skipFields.Contains(key) == false + ) + { + if (pro.PropertyType.IsNumber()) + { + var range = pro.GetCustomAttribute(); + int start = 0; + int end = 100; + if (range != null) + { + try + { + start = (int)Math.Truncate(double.Parse(range.Minimum.ToString())); + end = (int)Math.Truncate(double.Parse(range.Maximum.ToString())); + } + catch { } + } + Random r = new Random(); + val = r.Next(start, end).ToString(); + } + else if (pro.PropertyType.IsBoolOrNullableBool()) + { + List boolvalues = new List { "true", "false" }; + if (pro.PropertyType.IsNullable()) + { + boolvalues.Add("null"); + } + Random r = new Random(); + var index = r.Next(0, boolvalues.Count); + val = boolvalues[index]; + } + else if (pro.PropertyType.IsEnumOrNullableEnum()) + { + List enumvalues = new List(); + Type enumtype = null; + if (pro.PropertyType.IsNullable()) + { + enumtype = pro.PropertyType.GenericTypeArguments[0]; + enumvalues.Add("null"); + } + else + { + enumtype = pro.PropertyType; + } + var vs = Enum.GetValues(enumtype); + Random r = new Random(); + var index = r.Next(0, vs.Length); + val = enumtype.FullName+"."+ vs.GetValue(index).ToString(); + } + else if (pro.PropertyType == typeof(string)) + { + var length = pro.GetCustomAttribute(); + var min = 1; + var max = 20; + var l = 0; + if (length != null) + { + if (length.MaximumLength > 0) + { + max = length.MaximumLength; + } + if(length.MinimumLength > 0) + { + min = length.MinimumLength; + } + } + if(min == max) + { + l = max; + } + else if(min < max) + { + l = new Random().Next(min, max); + } + Random r = new Random(); + for (int i = 0; i < l; i++) + { + int index = r.Next(pat.Length); + val += pat[index]; + } + val = "\"" + val + "\""; + } + else if(pro.PropertyType == typeof(DateTime) || pro.PropertyType == typeof(DateTime?)) + { + Random r = new Random(); + val = DateTime.Now.AddDays(r.Next(-500, 500)).ToString("yyyy-MM-dd HH:mm:ss"); + val = $"DateTime.Parse(\"{val}\")"; + } + if (pros.Where(x => x.Name.ToLower() + "id" == key.ToLower()).Any()) + { + val = "$fk$"; + } + if (val != "") + { + if (rv.ContainsKey(key) == false) + { + rv.Add(key, val); + } + } + } + } + return rv; + } + + public static Dictionary GetRandomValuesForTestData(this Type self) + { + Dictionary rv = new Dictionary(); + string pat = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"; + var pros = self.GetAllProperties(); + List skipFields = new List() + { + nameof(TopBasePoco.BatchError), + nameof(TopBasePoco.Checked), + nameof(TopBasePoco.ExcelIndex), + }; + if (typeof(IBasePoco).IsAssignableFrom(self)) + { + skipFields.AddRange( + new string[]{ + nameof(IBasePoco.CreateBy), + nameof(IBasePoco.CreateTime), + nameof(IBasePoco.UpdateBy), + nameof(IBasePoco.UpdateTime) } + ); + } + if (typeof(IPersistPoco).IsAssignableFrom(self)) + { + skipFields.Add(nameof(IPersistPoco.IsValid)); + } + foreach (var pro in pros) + { + string key = pro.Name; + string val = ""; + var notmapped = pro.GetCustomAttribute(); + var required = pro.GetCustomAttributes() != null; + if (notmapped == null && + pro.PropertyType.IsList() == false && + pro.PropertyType.IsSubclassOf(typeof(TopBasePoco)) == false && + skipFields.Contains(key) == false + ) + { + if (pro.PropertyType.IsNumber()) + { + if (pro.Name == "ID") + { + val = ""; + } + else + { + var range = pro.GetCustomAttribute(); + int start = 0; + int end = 100; + if (range != null) + { + try + { + start = (int)Math.Truncate(double.Parse(range.Minimum.ToString())); + end = (int)Math.Truncate(double.Parse(range.Maximum.ToString())); + } + catch { } + } + Random r = new Random(); + val = r.Next(start, end).ToString(); + } + } + else if (pro.PropertyType.IsBoolOrNullableBool()) + { + List boolvalues = new List { "true", "false" }; + if (pro.PropertyType.IsNullable()) + { + if (required == false) + { + boolvalues.Add(""); + } + } + Random r = new Random(); + var index = r.Next(0, boolvalues.Count); + val = boolvalues[index]; + } + else if (pro.PropertyType.IsEnumOrNullableEnum()) + { + List enumvalues = new List(); + Type enumtype = null; + if (pro.PropertyType.IsNullable()) + { + enumtype = pro.PropertyType.GenericTypeArguments[0]; + if (required == false) + { + enumvalues.Add(""); + } + } + else + { + enumtype = pro.PropertyType; + } + var vs = Enum.GetValues(enumtype); + foreach (var item in vs) + { + enumvalues.Add((int)item + ""); + } + Random r = new Random(); + var index = r.Next(0, enumvalues.Count); + val = enumvalues[index]; + } + else if (pro.PropertyType == typeof(string)) + { + + var reg = pro.GetCustomAttribute(); + var length = pro.GetCustomAttribute(); + + if (reg != null) { + Xeger x = new Xeger(reg.Pattern); + val = x.Generate(); + if(length != null) + { + if(length.MaximumLength > 0 && val.Length > length.MaximumLength) + { + val = val.Substring(0, length.MaximumLength-1); + } + } + } + else + { + var min = 1; + var max = 20; + var l = 0; + if (length != null) + { + if (length.MaximumLength > 0) + { + max = length.MaximumLength; + } + if (length.MinimumLength > 0) + { + min = length.MinimumLength; + } + } + if (min == max) + { + l = max; + } + else if (min < max) + { + l = new Random().Next(min, max); + } + Random r = new Random(); + for (int i = 0; i < l; i++) + { + int index = r.Next(pat.Length); + val += pat[index]; + } + } + + val = "\"" + val + "\""; + } + else if (pro.PropertyType == typeof(DateTime) || pro.PropertyType == typeof(DateTime?)) + { + Random r = new Random(); + val = DateTime.Now.AddDays(r.Next(-500, 500)).ToString("yyyy-MM-dd HH:mm:ss"); + } + if (pros.Where(x => x.Name.ToLower() + "id" == key.ToLower()).Any()) + { + val = "$fk$"; + } + if (val != "") + { + if (rv.ContainsKey(key) == false) + { + rv.Add(key, val); + } + } + } + } + return rv; + } + + + public static PropertyInfo GetSingleProperty(this Type self, string name) + { + if (_propertyCache.ContainsKey(self.FullName) == false) + { + var properties = self.GetProperties().ToList(); + _propertyCache = _propertyCache.Add(self.FullName, properties); + return properties.Where(x => x.Name == name).FirstOrDefault(); + } + else + { + return _propertyCache[self.FullName].Where(x => x.Name == name).FirstOrDefault(); + } + } + + public static PropertyInfo GetSingleProperty(this Type self, Func where) + { + if (_propertyCache.ContainsKey(self.FullName) == false) + { + var properties = self.GetProperties().ToList(); + _propertyCache = _propertyCache.Add(self.FullName, properties); + return properties.Where(where).FirstOrDefault(); + } + else + { + return _propertyCache[self.FullName].Where(where).FirstOrDefault(); + } + } + + public static List GetAllProperties(this Type self) + { + if (_propertyCache.ContainsKey(self.FullName) == false) + { + var properties = self.GetProperties().ToList(); + _propertyCache = _propertyCache.Add(self.FullName, properties); + return properties; + } + else + { + return _propertyCache[self.FullName]; + } + } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/GlobalConstants.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/GlobalConstants.cs new file mode 100644 index 0000000..c2bb83b --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/GlobalConstants.cs @@ -0,0 +1,10 @@ +namespace WalkingTec.Mvvm.Core +{ + public static class GlobalConstants + { + public static class CacheKey + { + public const string UserInfo = "WtmUserInfo"; + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/GlobalData.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/GlobalData.cs new file mode 100644 index 0000000..e119c09 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/GlobalData.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using WalkingTec.Mvvm.Core.Support.Json; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// 应用全局缓存 + /// + public class GlobalData + { + + /// + /// 程序集 + /// + public List AllAssembly { get; set; } + + /// + /// 可访问的url地址 + /// + public List AllAccessUrls { get; set; } + + public Type CustomUserType { get; set; } + /// + /// 模块 + /// + public List AllModule { get; set; } + + private Func> MenuGetFunc; + + public List AllMenus => MenuGetFunc?.Invoke(); + + /// + /// 设置菜单委托 + /// + /// + public void SetMenuGetFunc(Func> func) => MenuGetFunc = func; + + public List GetTypesAssignableFrom() + { + var rv = new List(); + foreach (var ass in AllAssembly) + { + var types = new List(); + try + { + types.AddRange(ass.GetExportedTypes()); + } + catch { } + + rv.AddRange(types.Where(x => typeof(T).IsAssignableFrom(x) && x != typeof(T) && x.IsAbstract == false).ToList()); + } + return rv; + } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/GlobalServices.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/GlobalServices.cs new file mode 100644 index 0000000..39d0f50 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/GlobalServices.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// GlobalServices + /// + //public static class GlobalServices + //{ + // private static IServiceProvider _serviceProvider = null; + // /// + // /// IocContainer + // /// + // public static IServiceProvider ServiceProvider => _serviceProvider; + + // /// + // /// SetServiceProvider + // /// + // /// + // public static void SetServiceProvider(IServiceProvider container) { _serviceProvider = container; } + + // #region GetServicce + + // /// + // /// GetService + // /// + // /// + // /// + // public static T GetService() where T : class => ServiceProvider?.GetService(); + + // /// + // /// GetService + // /// + // /// + // /// + // public static object GetService(Type type) => ServiceProvider?.GetService(type); + + // /// + // /// GetServices + // /// + // /// + // /// + // public static IEnumerable GetServices() where T : class => ServiceProvider?.GetServices(); + + // /// + // /// GetServices + // /// + // /// + // /// + // public static IEnumerable GetServices(Type type) => ServiceProvider?.GetServices(type); + + // /// + // /// GetRequiredService + // /// + // /// + // /// + // public static T GetRequiredService() where T : class => ServiceProvider?.GetRequiredService(); + + // /// + // /// GetRequiredService + // /// + // /// + // /// + // public static object GetRequiredService(Type type) => ServiceProvider?.GetRequiredService(type); + + // #endregion + //} +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/GridAction.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/GridAction.cs new file mode 100644 index 0000000..2dba7cc --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/GridAction.cs @@ -0,0 +1,224 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Web; + +namespace WalkingTec.Mvvm.Core +{ + #region 列表动作类型 + /// + /// 列表动作类型 + /// + public enum GridActionParameterTypesEnum + { + /// + /// 不需要传递Id + /// + NoId, + /// + /// 只传递一个Id + /// + SingleId, + /// + /// 传递多个Id + /// + MultiIds, + /// + /// 只传递一个Id,但 Id 可能为null + /// + SingleIdWithNull, + /// + /// 传递多个 Id 或 null + /// + MultiIdWithNull, + AddRow, + RemoveRow + } + + #endregion + + + #region 标准列表动作 + /// + /// 标准列表动作 + /// + public enum GridActionStandardTypesEnum + { + Create, + Edit, + Delete, + SimpleDelete, + Details, + BatchEdit, + BatchDelete, + SimpleBatchDelete, + Import, + ExportExcel, + AddRow, + RemoveRow, + ActionsGroup + } + #endregion + + #region 导出枚举 + public enum ExportEnum + { + [Display(Name = "Excel")] + Excel = 0, + [Display(Name = "PDF")] + PDF = 1 + } + #endregion + + /// + /// 列表动作类,负责处理列表动作条中的动作按钮 + /// + public class GridAction + { + #region Action属性 + + /// + /// 按钮Id,一般不需要设定,系统会自动生成唯一Id。如果设定请确保 Id 的唯一性 + /// + public string ButtonId { get; set; } + + /// + /// 按钮名称 + /// + public string Name { get; set; } + + /// + /// 弹出窗口的标题 + /// + public string DialogTitle { get; set; } + + /// + /// 如果不为null,则只运行这个变量设定的script,其他的属性都不起作用 + /// + public string OnClickFunc { get; set; } + + /// + /// 是否在每行都显示 + /// + public bool ShowInRow { get; set; } + /// + /// 是否在工具栏上隐藏按钮 + /// + public bool HideOnToolBar { get; set; } + + /// + /// bind to a column name to deside whether or not to show this action + /// + public string BindVisiableColName { get; set; } + + /// + /// additional css class of button + /// + public string ButtonClass { get; set;} + /// + /// if the dialog need to be maximax + /// + public bool Max { get; set; } + + /// + /// If this action is to download a file + /// + public bool IsDownload { get; set; } + + #region 请求链接相关 + + /// + /// 动作的Area + /// + public string Area { get; set; } + + public bool IsExport { get; set; } + /// + /// 动作的Controller + /// + public string ControllerName { get; set; } + + /// + /// 动作的Action + /// + public string ActionName { get; set; } + + public string Url + { + get + { + var rv = ""; + if(string.IsNullOrEmpty(ControllerName) == false){ + rv = $"/{HttpUtility.UrlEncode(ControllerName)}/{HttpUtility.UrlEncode(ActionName)}"; + if (!string.IsNullOrEmpty(Area)) + { + rv = $"/{HttpUtility.UrlEncode(Area)}{rv}"; + } + if (!string.IsNullOrEmpty(QueryString)) + { + rv = $"{rv}?{QueryString}"; + } + else + { + rv = $"{rv}?1=1"; ; + } + } + return rv; + } + } + + #endregion + + /// + /// 是否跳转到新页面 + /// + public bool IsRedirect { get; set; } + + /// + /// 弹出问询框 + /// + public string PromptMessage { get; set; } + + /// + /// 动作类型 + /// + public GridActionParameterTypesEnum ParameterType { get; set; } + + public bool ForcePost { get; set; } + #endregion + + #region 暂时无用 + /// + /// 是否可以resizable + /// + public bool Resizable { get; set; } + /// + /// 动作图标css + /// + public string IconCls { get; set; } + /// + /// 动作的QueryString + /// + public string QueryString { get; set; } + /// + /// 弹出窗口的宽度 + /// + public int? DialogWidth { get; set; } + /// + /// 弹出窗口的高度 + /// + public int? DialogHeight { get; set; } + /// + /// 是否需要弹出窗口 + /// + public bool ShowDialog { get; set; } + + /// + /// 如果设定了SubActions,则代表需要用SplitButton的形式展示,主GridAction将不起作用 + /// + public List SubActions { get; set; } + + public string[] whereStr { get; set; } + + #endregion + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/GridActionExtension.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/GridActionExtension.cs new file mode 100644 index 0000000..739e20e --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/GridActionExtension.cs @@ -0,0 +1,424 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// GridActionExtension + /// + public static class GridActionExtensions + { + #region MakeStandardAction 创建标准动作 + + /// + /// 创建标准动作 + /// + /// 继承自TopBasePoco的类 + /// 继承自ISearcher的类 + /// self + /// 动作的Controller + /// 标准动作类型 + /// 弹出窗口的标题,可为空,代表使用默认文字 + /// 域名 + /// 弹出窗口的宽度 + /// 弹出窗口的高度 + /// 按钮显示的文字 + /// Button的id 默认自动生成 + /// whereStr + /// 列表动作 + /// + /// 根据标准动作类型,创建默认属性的标准动作 + /// + public static GridAction MakeStandardAction(this IBasePagedListVM self + , string controllerName + , GridActionStandardTypesEnum standardType + , string dialogTitle + , string areaName = null + , int? dialogWidth = null + , int? dialogHeight = null + , string name = null + , string buttonId = null + , params Expression>[] whereStr) + where T : TopBasePoco + where V : ISearcher + { + var iconcls = string.Empty; + var actionName = standardType.ToString(); + var gridname = string.Empty; + var paraType = GridActionParameterTypesEnum.NoId; + var showInRow = false; + var hideOnToolBar = false; + var showDialog = true; + var isexport = false; + string msg = null; + var ispost = false; + string qs = null; + switch (standardType) + { + case GridActionStandardTypesEnum.Create: + iconcls = "layui-icon layui-icon-add-1"; + gridname = CoreProgram._localizer?["Sys.Create"]; + paraType = GridActionParameterTypesEnum.NoId; + break; + case GridActionStandardTypesEnum.AddRow: + iconcls = "layui-icon layui-icon-add-1"; + gridname = CoreProgram._localizer?["Sys.Create"]; + paraType = GridActionParameterTypesEnum.AddRow; + break; + case GridActionStandardTypesEnum.Edit: + iconcls = "layui-icon layui-icon-edit"; + gridname = CoreProgram._localizer?["Sys.Edit"]; + paraType = GridActionParameterTypesEnum.SingleId; + showInRow = true; + hideOnToolBar = true; + break; + case GridActionStandardTypesEnum.Delete: + iconcls = "layui-icon layui-icon-delete"; + gridname = CoreProgram._localizer?["Sys.Delete"]; + paraType = GridActionParameterTypesEnum.SingleId; + showInRow = true; + hideOnToolBar = true; + break; + case GridActionStandardTypesEnum.SimpleDelete: + iconcls = "layui-icon layui-icon-delete"; + gridname = CoreProgram._localizer?["Sys.Delete"]; + paraType = GridActionParameterTypesEnum.SingleIdWithNull; + showInRow = true; + hideOnToolBar = true; + showDialog = false; + actionName = "BatchDelete"; + qs = "_donotuse_sd=1"; + ispost = true; + msg = CoreProgram._localizer?["Sys.DeleteConfirm"]; + break; + + case GridActionStandardTypesEnum.RemoveRow: + iconcls = "layui-icon layui-icon-delete"; + gridname = CoreProgram._localizer?["Sys.Delete"]; + paraType = GridActionParameterTypesEnum.RemoveRow; + showInRow = true; + hideOnToolBar = true; + break; + case GridActionStandardTypesEnum.Details: + iconcls = "layui-icon layui-icon-form"; + gridname = CoreProgram._localizer?["Sys.Details"]; + paraType = GridActionParameterTypesEnum.SingleId; + showInRow = true; + hideOnToolBar = true; + break; + case GridActionStandardTypesEnum.BatchEdit: + iconcls = "layui-icon layui-icon-edit"; + gridname = CoreProgram._localizer?["Sys.BatchEdit"]; + paraType = GridActionParameterTypesEnum.MultiIds; + break; + case GridActionStandardTypesEnum.BatchDelete: + iconcls = "layui-icon layui-icon-delete"; + gridname = CoreProgram._localizer?["Sys.BatchDelete"]; + paraType = GridActionParameterTypesEnum.MultiIds; + break; + case GridActionStandardTypesEnum.SimpleBatchDelete: + iconcls = "layui-icon layui-icon-delete"; + gridname = CoreProgram._localizer?["Sys.BatchDelete"]; + paraType = GridActionParameterTypesEnum.MultiIds; + showDialog = false; + msg = CoreProgram._localizer?["Sys.BatchDeleteConfirm"]; + actionName = "BatchDelete"; + ispost = true; + break; + case GridActionStandardTypesEnum.Import: + iconcls = "layui-icon layui-icon-templeate-1"; + gridname = CoreProgram._localizer?["Sys.Import"]; + paraType = GridActionParameterTypesEnum.NoId; + break; + case GridActionStandardTypesEnum.ExportExcel: + iconcls = "layui-icon layui-icon-download-circle"; + gridname = CoreProgram._localizer?["Sys.Export"]; + paraType = GridActionParameterTypesEnum.MultiIdWithNull; + name = CoreProgram._localizer?["Sys.ExportExcel"]; + showInRow = false; + showDialog = false; + hideOnToolBar = false; + isexport = true; + break; + default: + break; + } + + if (string.IsNullOrEmpty(dialogTitle)) + { + dialogTitle = gridname; + } + + var list = new List(); + foreach (var item in whereStr) + { + list.Add(PropertyHelper.GetPropertyName(item)); + } + + return new GridAction + { + ButtonId = buttonId, + Name = (name ?? gridname), + DialogTitle = dialogTitle, + Area = areaName, + ControllerName = controllerName, + ActionName = actionName, + ParameterType = paraType, + IconCls = iconcls, + DialogWidth = dialogWidth ?? 800, + DialogHeight = dialogHeight, + ShowInRow = showInRow, + ShowDialog = showDialog, + HideOnToolBar = hideOnToolBar, + PromptMessage = msg, + ForcePost = ispost, + QueryString = qs, + IsExport = isexport, + whereStr = list.ToArray() + }; + } + + #endregion + + #region MakeAction 创建按钮 + /// + /// 创建标准动作 + /// + /// 继承自TopBasePoco的类 + /// 继承自ISearcher的类 + /// self + /// 动作的Controller + /// actionName + /// 动作名,默认为‘新建’ + /// 弹出窗口的标题 + /// paraType + /// 域名 + /// 弹出窗口的宽度 + /// 弹出窗口的高度 + /// Button的id 默认自动生成 + /// whereStr + /// 列表动作 + /// + /// 根据标准动作类型,创建默认属性的标准动作 + /// + public static GridAction MakeAction(this IBasePagedListVM self + , string controllerName + , string actionName + , string name + , string dialogTitle + , GridActionParameterTypesEnum paraType + , string areaName = null + , int? dialogWidth = null + , int? dialogHeight = null + , string buttonId = null + , params Expression>[] whereStr) + where T : TopBasePoco + where V : ISearcher + { + var iconcls = string.Empty; + + var list = new List(); + foreach (var item in whereStr) + { + list.Add(PropertyHelper.GetPropertyName(item)); + } + + return new GridAction + { + ButtonId = buttonId, + Name = name, + DialogTitle = dialogTitle, + Area = areaName, + ControllerName = controllerName, + ActionName = actionName, + ParameterType = paraType, + IconCls = iconcls, + DialogWidth = dialogWidth ?? 800, + DialogHeight = dialogHeight, + ShowDialog = true, + whereStr = list.ToArray() + }; + } + + public static GridAction MakeActionsGroup(this IBasePagedListVM self + , string name + , List subActions + , params Expression>[] whereStr) + where T : TopBasePoco + where V : ISearcher + { + var iconcls = string.Empty; + + var list = new List(); + foreach (var item in whereStr) + { + list.Add(PropertyHelper.GetPropertyName(item)); + } + + return new GridAction + { + ButtonId = Guid.NewGuid().ToString(), + Name = name, + DialogTitle = "", + Area = "", + ControllerName = "", + ActionName = "ActionsGroup", + ParameterType = GridActionParameterTypesEnum.NoId, + IconCls = iconcls, + DialogWidth = 0, + DialogHeight = 0, + ShowDialog = false, + whereStr = list.ToArray(), + SubActions= subActions + }; + } + + #endregion + + #region MakeStandardExportAction 创建标准导出按钮 + + /// + /// 创建标准导出按钮 + /// + /// 继承自TopBasePoco的类 + /// 继承自ISearcher的类 + /// self + /// vmGuid + /// + /// 导出类型 默认null,支持所有导出 + /// 参数 + /// + [Obsolete("Will be removed in future, use MakeStandardAction with GridActionStandardTypesEnum.ExportExcel instead")] + public static GridAction MakeStandardExportAction(this IBasePagedListVM self + , string gridid = null + , bool MustSelect = false + , ExportEnum? exportType = null + , params KeyValuePair[] param) + where T : TopBasePoco + where V : ISearcher + { + exportType = ExportEnum.Excel; + + var action = new GridAction + { + Name = CoreProgram._localizer?["Sys.ExportExcel"], + DialogTitle = CoreProgram._localizer?["Sys.ExportExcel"], + Area = string.Empty, + ControllerName = "_Framework", + ActionName = "GetExportExcel", + ParameterType = GridActionParameterTypesEnum.MultiIdWithNull, + + IconCls = "layui-icon layui-icon-download-circle", + ShowInRow = false, + ShowDialog = false, + HideOnToolBar = false + }; + return action; + } + + #endregion + + #region Set Property + + /// + /// Set the dialog to be maximized + /// + /// + /// + /// + public static GridAction SetMax(this GridAction self, bool Max = true) + { + self.Max = Max; + return self; + } + + + /// + /// Set the dialog to be maximized + /// + /// + /// button class. + /// Some of the layui defined class to control color: + /// layui-btn-primary + /// layui-btn-normal + /// layui-btn-warm + /// layui-btn-danger + /// + /// + public static GridAction SetButtonClass(this GridAction self, string buttonclass) + { + self.ButtonClass = buttonclass; + return self; + } + + + + /// + /// Set the dialog to be maximized + /// + /// + /// + /// + public static GridAction SetIsDownload(this GridAction self, bool isDownload = true) + { + self.IsDownload = isDownload; + return self; + } + + public static GridAction SetIsExport(this GridAction self, bool isExport= true) + { + self.IsExport = isExport; + return self; + } + + + /// + /// Set prompt message + /// + /// + /// + /// + public static GridAction SetPromptMessage(this GridAction self, string msg) + { + self.PromptMessage = msg; + return self; + } + + + /// + /// 是否在每行都显示 + /// + /// + /// + /// + public static GridAction SetShowInRow(this GridAction self, bool showInRow = true) + { + self.ShowInRow = showInRow; + return self; + } + /// + /// 是否在工具栏上隐藏按钮 + /// + /// + /// + /// + public static GridAction SetHideOnToolBar(this GridAction self, bool hideOnToolBar = true) + { + self.HideOnToolBar = hideOnToolBar; + return self; + } + /// + /// 把按钮当作容器,添加按钮的子按钮 + /// + /// + /// 子按钮 + /// + public static GridAction SetSubActions(this GridAction self, List subActions) + { + self.SubActions = subActions; + return self; + } + #endregion + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/GridActionExtension`Old.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/GridActionExtension`Old.cs new file mode 100644 index 0000000..9f57c09 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/GridActionExtension`Old.cs @@ -0,0 +1,205 @@ +using System.Collections.Generic; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// GridActionExtension + /// + public static class GridActionExtension + { + /// + /// 按钮Id,一般不需要设定,系统会自动生成唯一Id。如果设定请确保 Id 的唯一性 + /// + /// + /// + /// + public static GridAction SetButtonId(this GridAction self, string buttonId) + { + self.ButtonId = buttonId; + return self; + } + /// + /// 按钮名称 + /// + /// + /// + /// + public static GridAction SetName(this GridAction self, string name) + { + self.Name = name; + return self; + } + /// + /// 弹出窗口的标题 + /// + /// + /// + /// + public static GridAction SetDialogTitle(this GridAction self, string dialogTitle) + { + self.DialogTitle = dialogTitle; + return self; + } + /// + /// 动作图标css + /// + /// + /// + /// + public static GridAction SetIconCls(this GridAction self, string iconCls) + { + self.IconCls = iconCls; + return self; + } + /// + /// 动作的Area + /// + /// + /// + /// + public static GridAction SetArea(this GridAction self, string area) + { + self.Area = area; + return self; + } + /// + /// 动作的Controller + /// + /// + /// + /// + public static GridAction SetControllerName(this GridAction self, string controllerName) + { + self.ControllerName = controllerName; + return self; + } + /// + /// 动作的Action + /// + /// + /// + /// + public static GridAction SetActionName(this GridAction self, string actionName) + { + self.ActionName = actionName; + return self; + } + /// + /// 动作的QueryString + /// + /// + /// + /// + public static GridAction SetQueryString(this GridAction self, string queryString) + { + self.QueryString = queryString; + return self; + } + /// + /// 弹出窗口的宽度、高度 + /// + /// + /// + /// + /// + public static GridAction SetSize(this GridAction self, int? dialogWidth, int? dialogHeight) + { + self.DialogWidth = dialogWidth; + self.DialogHeight = dialogHeight; + return self; + } + /// + /// 是否需要弹出窗口 + /// + /// + /// + /// + public static GridAction SetShowDialog(this GridAction self, bool showDialog = true) + { + self.ShowDialog = showDialog; + return self; + } + public static GridAction SetForcePost(this GridAction self, bool forcepost = true) + { + self.ForcePost = forcepost; + return self; + } + + + /// + /// 是否跳转到新页面 + /// + /// + /// + /// + public static GridAction SetIsRedirect(this GridAction self, bool isRedirect = true) + { + self.IsRedirect = isRedirect; + return self; + } + /// + /// 动作类型 + /// + /// + /// + /// + public static GridAction SetParameterType(this GridAction self, GridActionParameterTypesEnum parameterType) + { + self.ParameterType = parameterType; + return self; + } + /// + /// 如果不为null,则只运行这个变量设定的script,其他的属性都不起作用 + /// + /// + /// + /// + /// 如设置SetOnClickScript("test"),点击按钮时框架会调用页面上的javascript方法: function test(ids,datas){} + /// ids是勾选的id数组,datas是勾选的所有字段数组 + /// + /// + public static GridAction SetOnClickScript(this GridAction self, string onClickScript) + { + self.OnClickFunc = onClickScript; + return self; + } + /// + /// 如果设定了SubActions,则代表需要用SplitButton的形式展示,主GridAction将不起作用 + /// + /// + /// + /// + public static GridAction SetSubAction(this GridAction self, params GridAction[] gridActions) + { + if (self.SubActions == null) + { + self.SubActions = new List(); + } + self.SubActions.AddRange(gridActions); + return self; + } + /// + /// 是否可以拖动改变弹出窗体大小 + /// + /// + /// + /// + public static GridAction SetNotResizable(this GridAction self, bool resizable = false) + { + self.Resizable = resizable; + return self; + } + + /// + /// 设置一个布尔值的列名,当改列值为true的时候才显示本行的这个动作按钮 + /// + /// + /// + /// + public static GridAction SetBindVisiableColName(this GridAction self, string colName) + { + self.BindVisiableColName = colName; + return self; + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/GridColumn.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/GridColumn.cs new file mode 100644 index 0000000..321db3d --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/GridColumn.cs @@ -0,0 +1,510 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text.Json; +using WalkingTec.Mvvm.Core.Extensions; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// 列表列的定义 + /// + /// 列表的数据源类 + public class GridColumn : IGridColumn where T : TopBasePoco + { + public bool? ShowTotal { get; set; } + public GridColumn(Expression> columnExp, int? width) + { + ColumnExp = columnExp; + Width = width; + } + + /// + /// 表头类型 + /// + public GridColumnTypeEnum ColumnType { get; set; } + private string _field; + /// + /// 设定字段名 + /// + public string Field + { + get + { + if (_field == null) + { + _field = PI?.Name; + if (_field == null) + { + _field = CompiledCol?.Invoke(null).ToString(); + } + } + return _field; + } + set + { + _field = value; + } + } + + private string _title; + /// + /// 标题名称 + /// + public string Title + { + get + { + if (_title == null && PI != null) + { + _title = PropertyHelper.GetPropertyDisplayName(PI) ?? string.Empty; + } + return _title; + } + set + { + _title = value; + } + } + + /// + /// 列宽 + /// + public int? Width { get; set; } + /// + /// //监听单元格事件 + /// + public string Event { get; set; } + /// + /// 是否允许排序 + /// + public bool? Sort { get; set; } + + /// + /// 是否固定列 + /// + public GridColumnFixedEnum? Fixed { get; set; } + + /// + /// 对齐方式 + /// + public GridColumnAlignEnum Align { get; set; } + + /// + /// 是否可改变列宽 + /// + public bool? UnResize { get; set; } + + /// + /// 隐藏列 + /// + public bool? Hide { get; set; } + + /// + /// 是否禁止导出此列 + /// + public bool DisableExport { get; set; } + + /// + /// 子列 + /// + public IEnumerable> Children { get; set; } + private int? _childrenLen; + /// + /// 底层子列数量 + /// + public int ChildrenLength + { + get + { + if (_childrenLen == null) + { + var len = 0; + if (Children != null && Children.Any()) + { + len += Children.Where(x => x.Children == null || !x.Children.Any()).Count(); + var tempChildren = Children.Where(x => x.Children != null && x.Children.Any()).ToList(); + foreach (var item in tempChildren) + { + len += item.ChildrenLength; + } + } + _childrenLen = len; + } + return _childrenLen.Value; + } + } + + public EditTypeEnum? EditType { get; set; } + + public List ListItems { get; set; } + + #region 只读属性 生成 Excel 及其 表头用 + + /// + /// 递归获取子列数量最大层的子列数量 + /// + public int MaxChildrenCount + { + get + { + int rv = 1; + if (this.Children != null && Children.Any()) + { + rv = 0; + foreach (var child in this.Children) + { + rv += child.MaxChildrenCount; + } + } + return rv; + } + } + /// + /// 获取最大层数 + /// + public int MaxLevel + { + get + { + int rv = 1; + if (this.Children != null && Children.Any()) + { + int max = 0; + foreach (var child in this.Children) + { + int temp = child.MaxLevel; + if (max < temp) + { + max = temp; + } + } + rv += max; + } + return rv; + + } + } + + #endregion + + private PropertyInfo _pi; + protected PropertyInfo PI + { + get + { + if (_pi == null && ColumnExp != null) + { + _pi = PropertyHelper.GetPropertyInfo(ColumnExp); + } + return _pi; + } + } + + /// + /// ColumnExp + /// + public Expression> ColumnExp { get; set; } + + private int? _maxDepth; + + /// + /// 最大深度 + /// + public int MaxDepth + { + get + { + if (_maxDepth == null) + { + _maxDepth = 1; + if (Children?.Count() > 0) + { + _maxDepth += Children.Max(x => x.MaxDepth); + } + } + return _maxDepth.Value; + } + } + + #region 暂时没有用 + /// + /// + /// + public string Id { get; set; } + + private Func _compiledCol; + protected Func CompiledCol + { + get + { + if (_compiledCol == null) + { + if (ColumnExp == null) + { + _compiledCol = (T) => ""; + } + else + { + _compiledCol = ColumnExp.Compile(); + } + } + return _compiledCol; + } + } + + + private Type _fieldType; + /// + /// 获取值域类型 + /// + /// + public Type FieldType + { + get + { + return _fieldType ?? PI?.PropertyType; + } + set + { + _fieldType = value; + } + } + + public string FieldName + { + get + { + return PI?.Name; + } + } + + /// + /// 本列是否需要分组 + /// + public bool NeedGroup { get; set; } + public bool IsLocked { get; set; } + public bool Sortable { get; set; } + /// + /// 是否允许换行 + /// + public bool AllowMultiLine { get; set; } + /// + /// 设置某列是否应该尽量充满 + /// + public int? Flex { get; set; } + /// + /// 列内容的格式化函数 + /// + public ColumnFormatCallBack Format { get; set; } + + /// + /// 本列前景色函数 + /// + public Func ForeGroundFunc { get; set; } + /// + /// 本列背景色函数 + /// + public Func BackGroundFunc { get; set; } + + + /// + /// 获取最底层的子列 + /// + public IEnumerable> BottomChildren + { + get + { + List> rv = new List>(); + if (Children != null && Children.Any()) + { + foreach (var child in Children) + { + rv.AddRange(child.BottomChildren); + } + } + else + { + rv.Add(this); + } + return rv; + } + } + + /// + /// 根据设置前景色的函数返回前景色 + /// + /// 源数据 + /// 前景色 + public string GetForeGroundColor(object source) + { + if (ForeGroundFunc == null) + { + return ""; + } + else + { + return ForeGroundFunc.Invoke(source as T); + } + } + + /// + /// 根据设置背景色的函数返回背景色 + /// + /// 源数据 + /// 背景色 + public string GetBackGroundColor(object source) + { + if (BackGroundFunc == null) + { + return ""; + } + else + { + return BackGroundFunc.Invoke(source as T); + } + } + + + public bool HasFormat() + { + if(Format != null) + { + return true; + } + else + { + return false; + } + } + + /// + /// 获取单元格要输出的内容 + /// + /// 源数据 + /// 是否使用format + /// Html内容 + public virtual object GetText(object source, bool needFormat = true) + { + object rv = null; + var col = CompiledCol?.Invoke(source as T); + if(needFormat == false && Format != null) + { + var test = Format.Invoke(source as T, col); + if(test is ColumnFormatInfo == false && test is List == false) + { + return test??""; + } + } + + if (Format == null || needFormat == false) + { + if (col == null) + { + rv = ""; + } + else if (col is DateTime || col is DateTime?) + { + var datevalue = col as DateTime?; + if(datevalue != null) + { + if (datevalue.Value.Hour == 0 && datevalue.Value.Minute == 0 && datevalue.Value.Second == 0) + { + rv = datevalue.Value.ToString("yyyy-MM-dd"); + } + else + { + rv = datevalue.Value.ToString("yyyy-MM-dd HH:mm:ss"); + } + } + else + { + rv = ""; + } + } + else if (col.GetType().IsEnumOrNullableEnum()) + { + rv = col.ToString(); + } + else if (col.GetType().Namespace.Equals("System") == false) + { + if (needFormat == false) + { + rv = JsonSerializer.Serialize(col, CoreProgram.DefaultJsonOption); + } + else + { + rv = col.ToString(); + } + } + else + { + rv = col.ToString(); + } + } + else + { + rv = Format.Invoke(source as T, col); + } + if (rv == null) + { + rv = ""; + } + return rv; + } + + public virtual object GetObject(object source) + { + object rv = CompiledCol?.Invoke(source as T); + return rv; + } + + /// + /// 获取列头内容 + /// + /// Html内容 + protected virtual string GetHeader() + { + string rv = PropertyHelper.GetPropertyDisplayName(PI); + return rv ?? ""; + } + /// + /// 默认构造函数 + /// + public GridColumn() + { + AllowMultiLine = true; + this.Sortable = true; + } + /// + /// 构造函数 + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public GridColumn(Expression> ColumnExp, ColumnFormatCallBack Format = null, string Header = null, int? Width = null, int? Flex = null, bool AllowMultiLine = true, bool NeedGroup = false, Func ForeGroundFunc = null, Func BackGroundFunc = null, bool sortable = true) + { + this.ColumnExp = ColumnExp; + this.Format = Format; + this.Title = Header; + this.Width = Width; + this.NeedGroup = NeedGroup; + this.ForeGroundFunc = ForeGroundFunc; + this.BackGroundFunc = BackGroundFunc; + this.AllowMultiLine = AllowMultiLine; + this.Flex = Flex; + this.Sortable = sortable; + } + + #endregion + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/GridColumnExtension`Old.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/GridColumnExtension`Old.cs new file mode 100644 index 0000000..e82fc81 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/GridColumnExtension`Old.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// GridColumnExtension + /// + public static class GridColumnExtension + { + /// + /// GetAllBottomColumns + /// + /// + /// + /// + public static IEnumerable> GetAllBottomColumns(this IEnumerable> self) + { + List> rv = new List>(); + foreach (var item in self) + { + rv.AddRange(item.BottomChildren); + } + return rv; + } + /// + /// Id + /// + /// + /// + /// + /// + public static GridColumn SetId(this GridColumn self, string id) where T : TopBasePoco + { + self.Id = id; + return self; + } + /// + /// 列头 + /// + /// + /// + /// + /// + public static GridColumn SetHeader(this GridColumn self, string header) where T : TopBasePoco + { + self.Title = header; + return self; + } + /// + /// 本列是否需要分组 + /// + /// + /// + /// + /// + public static GridColumn SetNeedGroup(this GridColumn self, bool needGroup) where T : TopBasePoco + { + self.NeedGroup = needGroup; + return self; + } + public static GridColumn SetLocked(this GridColumn self, bool locked) where T : TopBasePoco + { + self.IsLocked = locked; + return self; + } + public static GridColumn SetSortable(this GridColumn self, bool sortable = false) where T : TopBasePoco + { + self.Sortable = sortable; + return self; + } + /// + /// 列宽 + /// + /// + /// + /// + /// + public static GridColumn SetWidth(this GridColumn self, int? width) where T : TopBasePoco + { + self.Width = width; + return self; + } + /// + /// 是否允许换行 + /// + /// + /// + /// + /// + public static GridColumn SetAllowMultiLine(this GridColumn self, bool allowMultiLine) where T : TopBasePoco + { + self.AllowMultiLine = allowMultiLine; + return self; + } + /// + /// 设置某列是否应该尽量充满 + /// + /// + /// + /// + /// + public static GridColumn SetFlex(this GridColumn self, int? flex) where T : TopBasePoco + { + self.Flex = flex; + return self; + } + ///// + ///// 时间与数字类型Format + ///// + ///// + ///// + ///// + ///// + //public static GridColumn SetFormat(this GridColumn self, string format) where T : TopBasePoco + //{ + // self.StringFormat = format; + // return self; + //} + /// + /// 列内容的格式化函数 + /// + /// + /// + /// + /// + public static GridColumn SetFormat(this GridColumn self, ColumnFormatCallBack format) where T : TopBasePoco + { + self.Format = format; + return self; + } + /// + /// 计算列值的表达式 + /// + /// + /// + /// + /// + public static GridColumn SetColumnExp(this GridColumn self, Expression> columnExp) where T : TopBasePoco + { + self.ColumnExp = columnExp; + return self; + } + /// + /// 子列 + /// + /// + /// + /// + /// + public static GridColumn SetChildren(this GridColumn self, params GridColumn[] childrens) where T : TopBasePoco + { + List> temp = new List>(); + if (self.Children == null) + { + temp = new List>(); + } + else + { + temp = self.Children.Cast>().ToList(); + } + temp.AddRange(childrens); + self.Children = temp; + return self; + } + /// + /// 本列前景色函数 + /// + /// + /// + /// + /// + public static GridColumn SetForeGroundFunc(this GridColumn self, Func foreGroundFunc) where T : TopBasePoco + { + self.ForeGroundFunc = foreGroundFunc; + return self; + } + /// + /// 本列背景色函数 + /// + /// + /// + /// + /// + public static GridColumn SetBackGroundFunc(this GridColumn self, Func backGroundFunc) where T : TopBasePoco + { + self.BackGroundFunc = backGroundFunc; + return self; + } + + /// + /// 设置本列是否显示汇总 + /// + /// + /// + /// 是否显示 + /// + public static GridColumn SetShowTotal(this GridColumn self, bool show = true) where T : TopBasePoco + { + self.ShowTotal = show; + return self; + } + + /// + /// 设置禁止导出此列数据 + /// + /// + /// + /// + public static GridColumn SetDisableExport(this GridColumn self) where T : TopBasePoco + { + self.DisableExport = true; + return self; + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/GridHeaderExtension.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/GridHeaderExtension.cs new file mode 100644 index 0000000..46dcde9 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/GridHeaderExtension.cs @@ -0,0 +1,310 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// GridColumn Extension + /// + public static class GridHeaderExtension + { + /// + /// 创建表头 + /// + /// + /// + /// + /// 绑定猎头表达式 + /// 宽度 + /// + public static GridColumn MakeGridHeader(this IBasePagedListVM self + , Expression> columnExp + , int? width = null + ) + where T : TopBasePoco + where V : ISearcher + { + MemberExpression me = null; + if (columnExp is MemberExpression) + { + me = columnExp as MemberExpression; + } + else if (columnExp is LambdaExpression) + { + var le = columnExp as LambdaExpression; + if (le.Body is MemberExpression) + { + me = le.Body as MemberExpression; + } + } + var alignType = GridColumnAlignEnum.Center; + if (me != null) + { + var propType = me.Type; + if (propType == typeof(string)) + { + alignType = GridColumnAlignEnum.Left; + } + } + + return new GridColumn(columnExp, width) { ColumnType = GridColumnTypeEnum.Normal, Align = alignType }; + } + + /// + /// 创建一个间隙列 + /// + /// + /// + /// + /// + public static GridColumn MakeGridHeaderSpace(this IBasePagedListVM self) + where T : TopBasePoco + where V : ISearcher + { + return new GridColumn() { ColumnType = GridColumnTypeEnum.Space }; + } + + /// + /// 创建一个父级表头 + /// + /// + /// + /// + /// 标题 + /// + public static GridColumn MakeGridHeaderParent(this IBasePagedListVM self, string title + ) + where T : TopBasePoco + where V : ISearcher + { + return new GridColumn() { Title = title }; + } + + public static GridColumn MakeGridHeaderAction(this IBasePagedListVM self + , string title = null + , int? width = 160 + , int? rowspan = null + ) + where T : TopBasePoco + where V : ISearcher + { + return new GridColumn() + { + ColumnType = GridColumnTypeEnum.Action, + Width = width, + Fixed = GridColumnFixedEnum.Right, + Title = title ?? CoreProgram._localizer?["Sys.Operation"] + }; + } + + #region GridColumn Property Setter + + /// + /// 设定字段名 + /// + /// + /// + /// 字段名的设定非常重要,是表格数据列的唯一标识,默认属性对应的名字 + /// + public static GridColumn SetField(this GridColumn self, string field) + where T : TopBasePoco + { + self.Field = field; + return self; + } + + /// + /// 设定标题名称 + /// + /// + /// + /// 即表头各列的标题,默认属性对应的 DisplayName 或 属性名 + /// + public static GridColumn SetTitle(this GridColumn self, string title) + where T : TopBasePoco + { + self.Title = title; + return self; + } + + /// + /// 设定列宽 + /// + /// + /// + /// 列宽的设定也通常是必须的(“特殊列”除外,如:复选框列、工具列等),它关系到表格的整体美观程度。 + /// + public static GridColumn SetWidth(this GridColumn self, int width) + where T : TopBasePoco + { + self.Width = width; + return self; + } + /// + /// 单元格事件名称 + /// + /// + /// + /// + /// + public static GridColumn SetEvent(this GridColumn self, string eEvent) where T : TopBasePoco + { + self.Event = eEvent; + return self; + } + + ///// + ///// 设定当前列头 列横跨的单元格数 + ///// + ///// + ///// + ///// 这种情况下不用设置 Field 和 Width + ///// + //public static GridColumn SetColspan(this GridColumn self, int colspan) + // where T : TopBasePoco + //{ + // self.Colspan = colspan; + // return self; + //} + + ///// + ///// 设定当前列头 纵向跨越的单元格数 + ///// + ///// + ///// + ///// 纵向跨越的单元格数 + ///// + //public static GridColumn SetRowspan(this GridColumn self, int rowspan) + // where T : TopBasePoco + //{ + // self.Rowspan = rowspan; + // return self; + //} + + /// + /// 设定是否允许排序 (ASCII码排序) + /// + /// + /// + /// 如果设置 true,则在对应的表头显示排序icon,从而对列开启排序功能。 + /// + public static GridColumn SetSort(this GridColumn self, bool sort = true) + where T : TopBasePoco + { + self.Sort = sort; + return self; + } + + /// + /// 设定是否固定列 + /// + /// + /// + /// 如果设置 Left 或 Right,则对应的列将会被固定在左或右,不随滚动条而滚动。 + /// + public static GridColumn SetFixed(this GridColumn self, GridColumnFixedEnum? @fixed) + where T : TopBasePoco + { + self.Fixed = @fixed; + return self; + } + + /// + /// 设定对齐方式 + /// + /// + /// + /// + /// + public static GridColumn SetAlign(this GridColumn self, GridColumnAlignEnum align) + where T : TopBasePoco + { + self.Align = align; + return self; + } + + ///// + ///// 设定是否允许编辑 + ///// + ///// + ///// + ///// 如果设置 true,则对应列的单元格将会被允许编辑,目前只支持type="text"的input编辑。 + ///// + //public static GridColumn SetEdit(this GridColumn self, bool edit = true) + // where T : TopBasePoco + //{ + // self.Edit = edit; + // return self; + //} + + ///// + ///// 设定自定义模板 + ///// + ///// + ///// + ///// 在默认情况下,单元格的内容是完全按照数据接口返回的content原样输出的, + ///// 如果你想对某列的单元格添加链接等其它元素,你可以借助该参数来轻松实现。 + ///// 这是一个非常实用的功能,你的表格内容会因此而丰富多样。 + ///// + ///// + //public static GridColumn SetTemplet(this GridColumn self, string templet) + // where T : TopBasePoco + //{ + // self.Templet = templet; + // return self; + //} + + [Obsolete("该方法已经被弃用,请使用 SetHide 代替")] + public static GridColumn SetHidden(this GridColumn self, bool hidden = true) where T : TopBasePoco + { + return self; + } + + /// + /// 是否隐藏 + /// + /// + /// + /// + /// + public static GridColumn SetHide(this GridColumn self, bool hide = true) where T : TopBasePoco + { + self.Hide = hide; + return self; + } + + /// + /// 设定列宽不可改变 + /// + /// + /// + /// + /// + public static GridColumn SetUnResize(this GridColumn self, bool unresize = true) + where T : TopBasePoco + { + self.UnResize = unresize; + return self; + } + + /// + /// 设定单元格编辑类型 + /// + /// + /// + /// 单元格编辑类型 + /// listitems + /// + public static GridColumn SetEditType(this GridColumn self, EditTypeEnum editType = EditTypeEnum.Text, List listitems = null) + where T : TopBasePoco + { + self.EditType = editType; + self.ListItems = listitems; + return self; + } + + #endregion + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/IGridColumn.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/IGridColumn.cs new file mode 100644 index 0000000..c435831 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Grid/IGridColumn.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Text.Json.Serialization; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// Grid Column Content Fixed Enum + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum GridColumnFixedEnum + { + /// + /// 规定在左侧 + /// + Left = 0, + /// + /// 规定在右侧 + /// + Right + } + + /// + /// Grid Column Edit Type Enum + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum EditTypeEnum + { + Text, + TextBox, + ComboBox, + Datetime, + CheckBox + } + + /// + /// Grid Column Content Align Enum + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum GridColumnAlignEnum + { + /// + /// Center + /// + Center = 0, + /// + /// Left + /// + Left, + /// + /// Right + /// + Right + } + + /// + /// Grid Column Type Enum + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum GridColumnTypeEnum + { + /// + /// 正常列 + /// + Normal = 0, + /// + /// 空列 + /// + Space, + /// + /// 操作列 + /// + Action + } + + /// + /// IGridColumn + /// + /// + public interface IGridColumn + { + /// + /// 表头类型 + /// + GridColumnTypeEnum ColumnType { get; set; } + + /// + /// 设定字段名 + /// + string Field { get; set; } + + /// + /// 标题名称 + /// + string Title { get; set; } + + /// + /// 列宽 + /// + int? Width { get; set; } + /// + /// //监听单元格事件 + /// + string Event { get; set; } + /// + /// 是否允许排序 + /// + bool? Sort { get; set; } + + /// + /// 是否固定列 + /// + GridColumnFixedEnum? Fixed { get; set; } + + /// + /// 对齐方式 + /// + GridColumnAlignEnum Align { get; set; } + + /// + /// 是否可改变列宽 + /// + bool? UnResize { get; set; } + + /// + /// 隐藏列 + /// + bool? Hide { get; set; } + + /// + /// 是否显示汇总 + /// + bool? ShowTotal { get; set; } + + /// + /// 子列 + /// + IEnumerable> Children { get; } + + /// + /// 底层子列数量 + /// + int ChildrenLength { get; } + + EditTypeEnum? EditType { get; set; } + + List ListItems { get; set; } + + #region 只读属性 生成 Excel 及其 表头用 + + /// + /// 最大子列数量 + /// + int MaxChildrenCount { get; } + + /// + /// 多表头的最大层数 + /// + int MaxLevel { get; } + + /// + /// 最下层列 + /// + IEnumerable> BottomChildren { get; } + + /// + /// 最大深度 + /// + int MaxDepth { get; } + + #endregion + + + #region 暂时没有用 + + string Id { get; set; } + + /// + /// 是否需要分组 + /// + bool NeedGroup { get; set; } + + bool IsLocked { get; set; } + + bool Sortable { get; set; } + /// + /// 是否允许多行 + /// + bool AllowMultiLine { get; set; } + /// + /// 是否填充 + /// + int? Flex { get; set; } + + Type FieldType { get; } + + string FieldName { get; } + + /// + /// 获取内容 + /// + /// 源数据 + /// 是否适用format + /// 内容 + object GetText(object source, bool needFormat = true); + + object GetObject(object source); + /// + /// 获取前景色 + /// + /// 源数据 + /// 前景色 + string GetForeGroundColor(object source); + /// + /// 获取背景色 + /// + /// 源数据 + /// 背景色 + string GetBackGroundColor(object source); + bool HasFormat(); + #endregion + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Helper/EntityHelper.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Helper/EntityHelper.cs new file mode 100644 index 0000000..65a7537 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Helper/EntityHelper.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Reflection; +using WalkingTec.Mvvm.Core.Extensions; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// DataTable和Entity之间转换的辅助类 + /// + public static class EntityHelper + { + /// + /// 根据DataTable获取Entity列表 + /// + /// Entity类型 + /// DataTable + /// Entity列表 + public static IList GetEntityList(DataTable table) + { + IList entityList = new List(); + + if (typeof(T) == typeof(DynamicData)) + { + foreach (DataRow row in table.Rows) + { + //新建Entity + T entity = (T)Activator.CreateInstance(typeof(T)); + foreach (DataColumn col in table.Columns) + { + (entity as DynamicData).Add(col.ColumnName, row[col] == DBNull.Value ? null : row[col]); + } + entityList.Add(entity); + } + } + else + { + var properties = typeof(T).GetAllProperties().ToLookup(property => property.Name, property => property).ToDictionary(i => i.Key, i => i.First()).Values; + + //循环Datable中的每一行 + foreach (DataRow row in table.Rows) + { + //新建Entity + T entity = (T)Activator.CreateInstance(typeof(T)); + //循环Entity的每一个属性 + foreach (var item in properties) + { + //如果DataTable中有列名和属性名一致,则把单元格内容赋值给Entity的该属性 + if (row.Table.Columns.Contains(item.Name)) + { + //判断null值 + if (string.IsNullOrEmpty(row[item.Name].ToString())) + { + item.SetValue(entity, null); + } + else + { + var ptype = item.PropertyType; + if (ptype.IsNullable()) + { + ptype = ptype.GenericTypeArguments[0]; + } + //如果是Guid或Guid?类型 + if (ptype == typeof(Guid)) + { + item.SetValue(entity, Guid.Parse(row[item.Name].ToString())); + } + //如果是enum或enum?类型 + else if (ptype.IsEnum) + { + item.SetValue(entity, Enum.ToObject(ptype, row[item.Name])); + } + else + { + item.SetValue(entity, Convert.ChangeType(row[item.Name], ptype)); + } + + } + } + } + entityList.Add(entity); + } + } + return entityList; + } + + #region 实体类转换成DataTable + + /// + /// 实体类转换成DataSet + /// + /// 实体类列表 + /// DataSet + public static DataSet ToDataSet(List modelList) where T : new() + { + if (modelList == null || modelList.Count == 0) + { + return null; + } + else + { + DataSet ds = new DataSet(); + ds.Tables.Add(ToDataTable(modelList)); + return ds; + } + } + + /// + /// 实体类转换成DataTable + /// + /// 实体类列表 + /// DataTable + public static DataTable ToDataTable(List modelList) where T : new() + { + if (modelList == null || modelList.Count == 0) + { + return null; + } + DataTable dt = CreateData(modelList[0]); + + foreach (T model in modelList) + { + DataRow dataRow = dt.NewRow(); + //循环实体类所有属性,给对应的DataTable字段赋值 + foreach (PropertyInfo propertyInfo in typeof(T).GetAllProperties()) + { + var res = propertyInfo.GetValue(model); + dataRow[propertyInfo.Name] = res ?? DBNull.Value; + } + dt.Rows.Add(dataRow); + } + return dt; + } + + /// + /// 根据实体类得到表结构 + /// + /// 实体类 + /// DataTable + private static DataTable CreateData(T model) where T : new() + { + DataTable dataTable = new DataTable(typeof(T).Name); + foreach (PropertyInfo propertyInfo in typeof(T).GetAllProperties()) + { + if (propertyInfo.PropertyType.IsGenericType) + { + if (propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>) && propertyInfo.PropertyType.GenericTypeArguments.Length > 0) + { + dataTable.Columns.Add(propertyInfo.Name, propertyInfo.PropertyType.GenericTypeArguments[0]); + continue; + } + else + { + continue; + } + } + dataTable.Columns.Add(propertyInfo.Name, propertyInfo.PropertyType); + } + return dataTable; + } + + #endregion + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Helper/IServiceExtension.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Helper/IServiceExtension.cs new file mode 100644 index 0000000..b029a6a --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Helper/IServiceExtension.cs @@ -0,0 +1,66 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using WalkingTec.Mvvm.Core.Extensions; +using WalkingTec.Mvvm.Core.Support.FileHandlers; + +namespace WalkingTec.Mvvm.Core +{ + public static class IServiceExtension + { + public static IServiceCollection AddWtmContextForConsole(this IServiceCollection services, string jsonFileDir = null, string jsonFileName = null, Func fileSubDirSelector = null) + { + var configBuilder = new ConfigurationBuilder(); + IConfigurationRoot ConfigRoot = configBuilder.WTMConfig(null,jsonFileDir,jsonFileName).Build(); + var WtmConfigs = ConfigRoot.Get(); + services.Configure(ConfigRoot); + services.AddLogging(builder => + { + builder.ClearProviders(); + builder.AddConfiguration(ConfigRoot.GetSection("Logging")) + .AddConsole() + .AddDebug() + .AddWTMLogger(); + }); + var gd = GetGlobalData(); + services.AddHttpContextAccessor(); + services.AddSingleton(gd); + WtmFileProvider._subDirFunc = fileSubDirSelector; + services.TryAddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddHttpClient(); + if (WtmConfigs.Domains != null) + { + foreach (var item in WtmConfigs.Domains) + { + services.AddHttpClient(item.Key, x => + { + x.BaseAddress = new Uri(item.Value.Url); + x.DefaultRequestHeaders.Add("Cache-Control", "no-cache"); + x.DefaultRequestHeaders.Add("User-Agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)"); + }); + } + } + services.AddDistributedMemoryCache(); + var cs = WtmConfigs.Connections; + foreach (var item in cs) + { + var dc = item.CreateDC(); + dc.Database.EnsureCreated(); + } + WtmFileProvider.Init(WtmConfigs, gd); + return services; + } + + private static GlobalData GetGlobalData() + { + GlobalData gd = new GlobalData(); + gd.AllAssembly = Utils.GetAllAssembly(); + return gd; + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Helper/LogDebug.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Helper/LogDebug.cs new file mode 100644 index 0000000..a9273fa --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Helper/LogDebug.cs @@ -0,0 +1,62 @@ +//using System; +//using System.Diagnostics; + +//namespace WalkingTec.Mvvm.Core +//{ +// /// +// /// LogDebug 只有定义了DEBUG常量才会生效,可以理解为只在Debug模式下才会输出 +// /// +// public sealed class LogDebug +// { +// /// +// /// Debug +// /// +// /// +// /// +// [Conditional("DEBUG")] +// public static void Debug(string message, Exception ex = null) +// { +// LogTrace.Debug(message, ex); +// } +// /// +// /// Info +// /// +// /// +// /// +// [Conditional("DEBUG")] +// public static void Info(string message, Exception ex = null) +// { +// LogTrace.Info(message, ex); +// } +// /// +// /// Warn +// /// +// /// +// /// +// [Conditional("DEBUG")] +// public static void Warn(string message, Exception ex = null) +// { +// LogTrace.Warn(message, ex); +// } +// /// +// /// Error +// /// +// /// +// /// +// [Conditional("DEBUG")] +// public static void Error(string message, Exception ex = null) +// { +// LogTrace.Error(message, ex); +// } +// /// +// /// Fatal +// /// +// /// +// /// +// [Conditional("DEBUG")] +// public static void Fatal(string message, Exception ex = null) +// { +// LogTrace.Fatal(message, ex); +// } +// } +//} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Helper/LogTrace.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Helper/LogTrace.cs new file mode 100644 index 0000000..80526cd --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Helper/LogTrace.cs @@ -0,0 +1,126 @@ +//using System; +//using System.Diagnostics; +//using System.Reflection; +//using log4net; + +//namespace WalkingTec.Mvvm.Core +//{ +// /// +// /// Trace 只有定义了TRACE常量才会生效,可以理解为在Debug与Release下均会输出 +// /// +// public sealed class LogTrace +// { +// private static ILog _logger; +// private static ILog Logger +// { +// get +// { +// if (_logger == null) +// { +// _logger = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); +// } +// return _logger; +// } +// } + +// /// +// /// Write +// /// +// /// +// /// +// public static void Write(string message, Exception ex = null) +// { +// OutputWindowWriteLine(message); +// if (ex == null) +// Logger.Debug(message); +// else +// Logger.Debug(message, ex); +// } +// /// +// /// Debug +// /// +// /// +// /// +// [Conditional("TRACE")] +// public static void Debug(string message, Exception ex = null) +// { +// Write(message, ex); +// } +// /// +// /// Info +// /// +// /// +// /// +// [Conditional("TRACE")] +// public static void Info(string message, Exception ex = null) +// { +// OutputWindowWriteLine(message); +// if (ex == null) +// Logger.Info(message); +// else +// Logger.Info(message, ex); +// } +// /// +// /// Warn +// /// +// /// +// /// +// [Conditional("TRACE")] +// public static void Warn(string message, Exception ex = null) +// { +// OutputWindowWriteLine(message); +// if (ex == null) +// Logger.Warn(message); +// else +// Logger.Warn(message, ex); +// } +// /// +// /// Error +// /// +// /// +// /// +// [Conditional("TRACE")] +// public static void Error(string message, Exception ex = null) +// { +// OutputWindowWriteLine(message); +// if (ex == null) +// Logger.Error(message); +// else +// Logger.Error(message, ex); +// } +// /// +// /// Fatal +// /// +// /// +// /// +// [Conditional("TRACE")] +// public static void Fatal(string message, Exception ex = null) +// { +// OutputWindowWriteLine(message); +// if (ex == null) +// Logger.Fatal(message); +// else +// Logger.Fatal(message, ex); +// } + +// #region Output Window +// [Conditional("DEBUG")] +// private static void OutputWindowWriteLine(string message, int level = 0, string category = null) +// { +// if (System.Diagnostics.Debugger.IsLogging()) +// { +// //调用Debugger.Log方法,这个方法可以输出信息到DebugView中 +// System.Diagnostics.Debugger.Log(level, category, $"{message}\r\n"); +// } +// } +// [Conditional("DEBUG")] +// private static void OutputWindowWrite(string message, int level = 0, string category = null) +// { +// if (System.Diagnostics.Debugger.IsLogging()) +// { +// System.Diagnostics.Debugger.Log(level, category, message); +// } +// } +// #endregion +// } +//} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Helper/PropertyHelper.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Helper/PropertyHelper.cs new file mode 100644 index 0000000..095b583 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Helper/PropertyHelper.cs @@ -0,0 +1,826 @@ +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Primitives; +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text.RegularExpressions; +using WalkingTec.Mvvm.Core.Extensions; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// 属性辅助类 + /// + public static class PropertyHelper + { + + public static object GetExpressionRootObj(Expression expression) + { + if (expression == null) + { + return ""; + } + Expression me = null; + LambdaExpression le = null; + if (expression is MemberExpression) + { + me = expression as MemberExpression; + } + if (expression is LambdaExpression) + { + le = expression as LambdaExpression; + if (le.Body is MemberExpression) + { + me = le.Body as MemberExpression; + } + if (le.Body is UnaryExpression) + { + me = (le.Body as UnaryExpression).Operand as MemberExpression; + } + } + while (me != null && me.NodeType == ExpressionType.MemberAccess) + { + Expression exp = (me as MemberExpression).Expression; + if (exp is MemberExpression) + { + me = exp as MemberExpression; + } + else if (exp is MethodCallExpression) + { + var mexp = exp as MethodCallExpression; + if (mexp.Method.Name == "get_Item") + { + object index = 0; + if (mexp.Arguments[0] is MemberExpression) + { + var obj = ((mexp.Arguments[0] as MemberExpression).Expression as ConstantExpression).Value; + index = obj.GetType().GetField((mexp.Arguments[0] as MemberExpression).Member.Name).GetValue(obj); + } + else + { + index = (mexp.Arguments[0] as ConstantExpression).Value; + } + me = mexp.Object as MemberExpression; + } + } + else + { + me = exp; + break; + } + } + if(me.NodeType == ExpressionType.Constant) + { + return (me as ConstantExpression)?.Value; + } + return null; + } + + public static Func GetPropertyExpression(Type objtype, string property) + { + property = Regex.Replace(property, @"\[[^\]]*\]", string.Empty); + List level = new List(); + if (property.Contains('.')) + { + level.AddRange(property.Split('.')); + } + else + { + level.Add(property); + } + + var pe = Expression.Parameter(objtype); + var member = Expression.Property(pe, objtype.GetSingleProperty(level[0])); + for (int i = 1; i < level.Count; i++) + { + member = Expression.Property(member, member.Type.GetSingleProperty(level[i])); + } + return Expression.Lambda>(member, pe).Compile(); + } + + /// + /// 获取属性名 + /// + /// 属性表达式 + /// 是否获取全部级别名称,比如a.b.c + /// 属性名 + public static string GetPropertyName(this Expression expression, bool getAll = true) + { + if (expression == null) + { + return ""; + } + MemberExpression me = null; + LambdaExpression le = null; + if (expression is MemberExpression) + { + me = expression as MemberExpression; + } + if (expression is LambdaExpression) + { + le = expression as LambdaExpression; + if (le.Body is MemberExpression) + { + me = le.Body as MemberExpression; + } + if (le.Body is UnaryExpression) + { + me = (le.Body as UnaryExpression).Operand as MemberExpression; + } + } + string rv = ""; + if (me != null) + { + rv = me.Member.Name; + } + while (me != null && getAll && me.NodeType == ExpressionType.MemberAccess) + { + Expression exp = me.Expression; + if (exp is MemberExpression) + { + rv = (exp as MemberExpression).Member.Name + "." + rv; + me = exp as MemberExpression; + } + else if (exp is MethodCallExpression) + { + var mexp = exp as MethodCallExpression; + if (mexp.Method.Name == "get_Item") + { + object index = 0; + if (mexp.Arguments[0] is MemberExpression) + { + var obj = ((mexp.Arguments[0] as MemberExpression).Expression as ConstantExpression).Value; + index = obj.GetType().GetField((mexp.Arguments[0] as MemberExpression).Member.Name).GetValue(obj); + } + else + { + index = (mexp.Arguments[0] as ConstantExpression).Value; + } + rv = (mexp.Object as MemberExpression).Member.Name + "[" + index + "]." + rv; + me = mexp.Object as MemberExpression; + } + } + else + { + break; + } + } + return rv; + } + + public static Expression GetMemberExp(this ParameterExpression self, Expression member) + { + return self.GetMemberExp(member.GetPropertyName()); + } + + public static Expression GetMemberExp(this ParameterExpression self, string memberName) + { + var names = memberName.Split(','); + Expression rv = Expression.PropertyOrField(self, names[0]); ; + for (int i = 1; i < names.Length; i++) + { + rv = Expression.PropertyOrField(rv, names[i]); + } + return rv; + } + + + /// + /// 获取属性名的Id形式,将属性名中的.转换为_,适合作为HTML中的Id使用 + /// + /// 属性表达式 + /// 是否获取全部级别名称,比如a.b.c + /// 属性Id + public static string GetPropertyId(this Expression expression, bool getAll = true) + { + return GetPropertyName(expression, getAll).GetIdByName(); + } + + /// + /// 获取正则表达式错误 + /// + /// 属性信息 + /// 错误文本 + public static string GetRegexErrorMessage(this MemberInfo pi) + { + string rv = ""; + + if (pi.GetCustomAttributes(typeof(RegularExpressionAttribute), false).FirstOrDefault() is RegularExpressionAttribute dis && !string.IsNullOrEmpty(dis.ErrorMessage)) + { + rv = dis.ErrorMessage; + if (CoreProgram._localizer != null) + { + rv = CoreProgram._localizer[rv]; + } + } + else + { + rv = ""; + } + return rv; + } + + /// + /// 获取属性显示名称 + /// + /// 属性信息 + /// + /// 属性名称 + public static string GetPropertyDisplayName(this MemberInfo pi, IStringLocalizer local = null) + { + string rv = ""; + if (pi.GetCustomAttributes(typeof(DisplayAttribute), false).FirstOrDefault() is DisplayAttribute dis && !string.IsNullOrEmpty(dis.Name)) + { + rv = dis.Name; + if (local == null) + { + if (CoreProgram._localizer != null) + { + rv = CoreProgram._localizer[rv]; + } + } + else + { + rv = local[rv]; + } + } + else + { + rv = pi.Name; + } + return rv; + } + + /// + /// 获取属性显示名称 + /// + /// 属性表达式 + /// + /// 属性显示名称 + public static string GetPropertyDisplayName(this Expression expression, IStringLocalizer local = null) + { + return GetPropertyDisplayName(expression.GetPropertyInfo(), local); + } + + + /// + /// 获取枚举显示名称 + /// + /// 枚举值 + /// 枚举显示名称 + public static string GetEnumDisplayName(this Enum value) + { + return GetEnumDisplayName(value.GetType(), value.ToString()); + } + + /// + /// 获取属性信息 + /// + /// 属性表达式 + /// 属性信息 + public static PropertyInfo GetPropertyInfo(this Expression expression) + { + MemberExpression me = null; + LambdaExpression le = null; + if (expression is MemberExpression) + { + me = expression as MemberExpression; + } + if (expression is LambdaExpression) + { + le = expression as LambdaExpression; + if (le.Body is MemberExpression) + { + me = le.Body as MemberExpression; + } + if (le.Body is UnaryExpression) + { + me = (le.Body as UnaryExpression).Operand as MemberExpression; + } + } + PropertyInfo rv = null; + if (me != null) + { + rv = me.Member.DeclaringType.GetSingleProperty(me.Member.Name); + } + return rv; + } + + /// + /// 获取属性值 + /// + /// 属性表达式 + /// 属性所在实例 + /// 属性值 + public static object GetPropertyValue(this object obj, LambdaExpression exp) + { + //获取表达式的值,并过滤单引号 + try + { + var expValue = exp.Compile().DynamicInvoke(obj); + object val = expValue; + return val; + } + catch + { + return ""; + } + } + + public static object GetPropertyValue(this object obj, string property) + { + //获取表达式的值,并过滤单引号 + try + { + return obj.GetType().GetSingleProperty(property).GetValue(obj); ; + } + catch + { + return ""; + } + } + + + public static List GetPropertySiblingValues(this object obj, string propertyName) + { + if(obj == null) + { + return new List(); + } + Regex reg = new Regex("(.*?)\\[\\-?\\d?\\]\\.(.*?)$"); + var match = reg.Match(propertyName); + if (match.Success) + { + var name1 = match.Groups[1].Value; + var name2 = match.Groups[2].Value; + + var levels = name1.Split('.'); + var objtype = obj.GetType(); + var pe = Expression.Parameter(objtype); + var member = Expression.Property(pe, objtype.GetSingleProperty(levels[0])); + for (int i = 1; i < levels.Length; i++) + { + member = Expression.Property(member, member.Type.GetSingleProperty(levels[i])); + } + var pe2 = Expression.Parameter(member.Type.GetGenericArguments()[0]); + var cast = Expression.Call(typeof(Enumerable), "Cast", new Type[] { pe2.Type }, member); + + var name2exp = Expression.Property(pe2, pe2.Type.GetSingleProperty(name2)); + var selectexp = Expression.Call(name2exp, "ToString", Type.EmptyTypes); + + Expression select = Expression.Call( + typeof(Enumerable), + "Select", + new Type[] { pe2.Type, typeof(string) }, + cast, + Expression.Lambda(selectexp, pe2)); + + + var lambda = Expression.Lambda(select, pe); + var rv = new List(); + try + { + rv = (lambda.Compile().DynamicInvoke(obj) as IEnumerable)?.ToList(); + } + catch { } + return rv; + } + else + { + return new List(); + } + } + + /// + /// 判断属性是否必填 + /// + /// 属性信息 + /// 是否必填 + public static bool IsPropertyRequired(this MemberInfo pi) + { + bool isRequired = false; + if (pi != null) + { + //如果需要显示星号,则判断是否是必填项,如果是必填则在内容后面加上星号 + //所有int,float。。。这种Primitive类型的,肯定都是必填 + Type t = pi.GetMemberType(); + if (t != null && (t.IsPrimitive() || t.IsEnum() || t == typeof(decimal) || t == typeof(Guid))) + { + isRequired = true; + } + else + { + //对于其他类,检查是否有RequiredAttribute,如果有就是必填 + if (pi.GetCustomAttributes(typeof(RequiredAttribute), false).FirstOrDefault() is RequiredAttribute required && required.AllowEmptyStrings == false) + { + isRequired = true; + } + else if (pi.GetCustomAttributes(typeof(KeyAttribute), false).FirstOrDefault() != null) + { + isRequired = true; + } + } + } + return isRequired; + } + + /// + /// 设置属性值 + /// + /// 属性所在实例 + /// 属性名 + /// 要赋的值 + /// 属性前缀 + /// 是否为字符串格式的值 + public static void SetPropertyValue(this object source, string property, object value, string prefix = null, bool stringBasedValue = false) + { + try + { + property = Regex.Replace(property, @"\[[^\]]*\]", string.Empty); + List level = new List(); + if (property.Contains('.')) + { + level.AddRange(property.Split('.')); + } + else + { + level.Add(property); + } + + if (!string.IsNullOrWhiteSpace(prefix)) + { + level.Insert(0, prefix); + } + object temp = source; + Type tempType = source.GetType(); + for (int i = 0; i < level.Count - 1; i++) + { + var member = tempType.GetMember(level[i])[0]; + if (member != null) + { + var va = member.GetMemberValue(temp); + if (va != null) + { + temp = va; + } + else + { + var newInstance = member.GetMemberType().GetConstructor(Type.EmptyTypes).Invoke(null); + member.SetMemberValue(temp, newInstance, null); + temp = newInstance; + } + tempType = member.GetMemberType(); + + } + } + + var memberInfos = tempType.GetMember(level.Last()); + if (!memberInfos.Any()) + { + return; + } + var fproperty = memberInfos[0]; + if (value == null || ((value is StringValues s) && StringValues.IsNullOrEmpty(s))) + { + fproperty.SetMemberValue(temp, null, null); + return; + } + + bool isArray = false; + if (value != null && value.GetType().IsArray == true) + { + isArray = true; + } + + if (stringBasedValue == true) + { + Type propertyType = fproperty.GetMemberType(); + if (propertyType.IsGeneric(typeof(List<>)) == true) + { + var list = propertyType.GetConstructor(Type.EmptyTypes).Invoke(null) as IList; + + var gs = propertyType.GenericTypeArguments; + try + { + if (value.GetType() == typeof(StringValues)) + { + var strVals = (StringValues)value; + var a = strVals.ToArray(); + for (int i = 0; i < a.Length; i++) + { + list.Add(a[i].ConvertValue(gs[0])); + } + } + else if (isArray) + { + var a = (value as object[]); + for (int i = 0; i < a.Length; i++) + { + list.Add(a[i].ConvertValue(gs[0])); + } + } + else + { + list = value.ConvertValue(propertyType) as IList; + } + } + catch { } + fproperty.SetMemberValue(temp, list, null); + } + else if (propertyType.IsArray) + { + try + { + var strVals = (StringValues)value; + var eletype = propertyType.GetElementType(); + var arr = Array.CreateInstance(eletype, strVals.Count); + for (int i = 0; i < arr.Length; i++) + { + arr.SetValue(strVals[i].ConvertValue(eletype), i); + } + fproperty.SetMemberValue(temp, arr, null); + } + catch { } + } + else + { + if (isArray) + { + var a = (value as object[]); + if (a.Length == 1) + { + value = a[0]; + } + } + + if (value is string) + { + value = value.ToString().Replace("\\", "/"); + } + fproperty.SetMemberValue(temp, value, null); + } + } + else + { + if (value is string) + { + value = value.ToString().Replace("\\", "/"); + } + fproperty.SetMemberValue(temp, value, null); + } + } + catch + { + } + } + + /// + /// 根据MemberInfo获取值 + /// + /// MemberInfo + /// 所在实例 + /// 如果是数组,指定数组下标。默认为null + /// MemberInfo的值 + public static object GetMemberValue(this MemberInfo mi, object obj, object[] index = null) + { + object rv = null; + if (mi.MemberType == MemberTypes.Property) + { + rv = ((PropertyInfo)mi).GetValue(obj, index); + } + else if (mi.MemberType == MemberTypes.Field) + { + rv = ((FieldInfo)mi).GetValue(obj); + } + return rv; + } + + /// + /// 设定MemberInfo的值 + /// + /// MemberInfo + /// 所在实例 + /// 要赋的值 + /// 如果是数组,指定数组下标。默认为null + public static void SetMemberValue(this MemberInfo mi, object obj, object val, object[] index = null) + { + object newval = val; + if (val is string s) + { + if (string.IsNullOrEmpty(s)) + { + val = null; + } + } + if (val != null && val.GetType() != mi.GetMemberType()) + { + newval = val.ConvertValue(mi.GetMemberType()); + } + if (mi.MemberType == MemberTypes.Property) + { + ((PropertyInfo)mi).SetValue(obj, newval, index); + } + else if (mi.MemberType == MemberTypes.Field) + { + ((FieldInfo)mi).SetValue(obj, newval); + } + } + + /// + /// 获取某个MemberInfo的类型 + /// + /// MemberInfo + /// 类型 + public static Type GetMemberType(this MemberInfo mi) + { + Type rv = null; + if (mi != null) + { + if (mi.MemberType == MemberTypes.Property) + { + rv = ((PropertyInfo)mi).PropertyType; + } + else if (mi.MemberType == MemberTypes.Field) + { + rv = ((FieldInfo)mi).FieldType; + } + } + return rv; + } + + /// + /// 获取枚举显示名称 + /// + /// 枚举类型 + /// 枚举值 + /// 枚举显示名称 + public static string GetEnumDisplayName(Type enumType, string value) + { + string rv = ""; + FieldInfo field = null; + + if (enumType.IsEnum()) + { + field = enumType.GetField(value); + } + //如果是nullable的枚举 + if (enumType.IsGeneric(typeof(Nullable<>)) && enumType.GetGenericArguments()[0].IsEnum()) + { + field = enumType.GenericTypeArguments[0].GetField(value); + } + + if (field != null) + { + + var attribs = field.GetCustomAttributes(typeof(DisplayAttribute), true).ToList(); + if (attribs.Count > 0) + { + rv = ((DisplayAttribute)attribs[0]).GetName(); + if (CoreProgram._localizer != null) + { + rv = CoreProgram._localizer[rv]; + } + } + else + { + rv = value; + } + } + return rv; + } + + public static string GetEnumDisplayName(Type enumType, int value) + { + string rv = ""; + FieldInfo field = null; + string ename = ""; + if (enumType.IsEnum()) + { + ename = enumType.GetEnumName(value); + field = enumType.GetField(ename); + } + //如果是nullable的枚举 + if (enumType.IsGeneric(typeof(Nullable<>)) && enumType.GetGenericArguments()[0].IsEnum()) + { + ename = enumType.GenericTypeArguments[0].GetEnumName(value); + field = enumType.GenericTypeArguments[0].GetField(ename); + } + + if (field != null) + { + + var attribs = field.GetCustomAttributes(typeof(DisplayAttribute), true).ToList(); + if (attribs.Count > 0) + { + rv = ((DisplayAttribute)attribs[0]).GetName(); + if (CoreProgram._localizer != null) + { + rv = CoreProgram._localizer[rv]; + } + } + else + { + rv = ename; + } + } + return rv; + } + + /// + /// 转化值 + /// + /// 要转换的值 + /// 转换后的类型 + /// 转换后的值 + public static object ConvertValue(this object value, Type propertyType) + { + object val = null; + if (propertyType.IsGeneric(typeof(Nullable<>)) == true) + { + var gs = propertyType.GenericTypeArguments; + try + { + val = ConvertValue(value, gs[0]); + } + catch { } + } + else if (propertyType.IsEnum()) + { + val = Enum.Parse(propertyType, value.ToString()); + } + else if (propertyType == typeof(string)) + { + val = value?.ToString().Trim(); + } + else if (propertyType == typeof(Guid)) + { + bool suc = Guid.TryParse(value?.ToString(), out Guid g); + if (suc) + { + val = g; + } + else + { + val = Guid.Empty; + } + } + else if (propertyType == typeof(DateRange)) + { + if (DateRange.TryParse(value.ToString(), out var result)) + { + val = result; + } + else + { + val = DateRange.Default; + } + } + else + { + try + { + if (value.ToString().StartsWith("`") && value.ToString().EndsWith("`")) + { + string inner = value.ToString().Trim('`').TrimEnd(','); + if (!string.IsNullOrWhiteSpace(inner)) + { + val = propertyType.GetConstructor(Type.EmptyTypes).Invoke(null); + string[] pair = inner.Split(','); + var gs = propertyType.GetGenericArguments(); + foreach (var p in pair) + { + (val as IList).Add(Convert.ChangeType(p, gs[0])); + } + } + } + else + { + val = Convert.ChangeType(value.ToString(), propertyType); + } + } + catch + { + } + } + return val; + } + + public static object MakeList(Type innerType, string propertyName, object[] values) + { + object rv = typeof(List<>).MakeGenericType(innerType).GetConstructor(Type.EmptyTypes).Invoke(null); + var mi = rv.GetType().GetMethod("Add"); + var con = innerType.GetConstructor(Type.EmptyTypes); + foreach (var item in values) + { + var newobj = con.Invoke(null); + newobj.SetPropertyValue(propertyName, item); + mi.Invoke(rv, new object[] { newobj }); + } + return rv; + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/IBasePagedListVM.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/IBasePagedListVM.cs new file mode 100644 index 0000000..4e19922 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/IBasePagedListVM.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// ListVM接口 + /// + /// ListVM中的Model类 + /// ListVM使用的Searcher类 + public interface IBasePagedListVM : IBaseVM + where T : TopBasePoco + where S : ISearcher + { + Type ModelType { get; } + + /// + /// 多级表头深度 默认 1级 + /// + int GetChildrenDepth(); + + /// + /// GetHeaders + /// + /// + + IEnumerable> GetHeaders(); + + /// + /// 页面动作 + /// + List GetGridActions(); + + /// + /// 查询并生成Excel + /// + /// Excel文件 + byte[] GenerateExcel(); + + string TotalText { get; set; } + #region Old + event Action> OnAfterInitList; + /// + ///记录批量操作时列表中选择的Id + /// + List Ids { get; set; } + string SelectorValueField { get; set; } + + /// + /// 获取Model集合 + /// + /// Model集合 + IEnumerable GetEntityList(); + + void ClearEntityList(); + /// + /// 获取Searcher + /// + S Searcher { get; } + /// + /// GetIsSelected + /// + /// + /// + bool GetIsSelected(object item); + + /// + /// 是否已经搜索过 + /// + bool IsSearched { get; set; } + + /// + /// PassSearch + /// + bool PassSearch { get; set; } + + /// + /// 搜索模式 + /// + ListVMSearchModeEnum SearcherMode { get; set; } + + /// + /// 是否需要分页 + /// + bool NeedPage { get; set; } + + /// + /// 允许导出Excel的最大行数,超过行数会分成多个文件,最多不能超过100万 + /// + int ExportMaxCount { get; set; } + + /// + /// 根据允许导出的Excel最大行数,算出最终导出的Excel个数 + /// + int ExportExcelCount { get; set; } + + /// + /// 移除操作列 + /// + void RemoveActionColumn(object root = null); + + void RemoveAction(); + + /// + /// 填加错误信息列,用于批量操作的列表 + /// + void AddErrorColumn(); + + /// + /// 搜索条件Panel的Id + /// + string SearcherDivId { get; } + + /// + /// GetSearchQuery + /// + /// + IOrderedQueryable GetSearchQuery(); + /// + /// DoSearch + /// + void DoSearch(); + /// + /// CopyContext + /// + /// + void CopyContext(BaseVM vm); + + /// + /// ReplaceWhere + /// + Expression ReplaceWhere { get; set; } + + /// + /// SetFullRowColor + /// + /// + /// + string SetFullRowColor(object entity); + /// + /// SetFullRowBgColor + /// + /// + /// + string SetFullRowBgColor(object entity); + + T CreateEmptyEntity(); + + /// + /// 用于为子表生成可编辑Grid时,内部控件名称前缀 + /// + string DetailGridPrix { get; set; } + + void DoInitListVM(); + + #endregion + + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/IBaseVM.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/IBaseVM.cs new file mode 100644 index 0000000..477cafd --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/IBaseVM.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Caching.Distributed; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// IBaseVM + /// + public interface IBaseVM + { + #region Property + + WTMContext Wtm { get; set; } + + /// + /// UniqueId + /// + string UniqueId { get; } + /// + /// WindowIds + /// + string WindowIds { get;} + /// + /// ViewDivId + /// + string ViewDivId { get; set; } + /// + /// DC + /// + IDataContext DC { get; set; } + /// + /// VMFullName + /// + string VMFullName { get; } + /// + /// CreatorAssembly + /// + string CreatorAssembly { get; set; } + /// + /// CurrentCS + /// + string CurrentCS { get; } + /// + /// FC + /// + Dictionary FC { get; set; } + /// + /// Config + /// + Configs ConfigInfo { get; } + + ISessionService Session { get; } + + IDistributedCache Cache { get; } + + LoginUserInfo LoginUserInfo { get; } + #endregion + + #region Event + + /// + /// InitVM 完成后触发的事件 + /// + event Action OnAfterInit; + /// + /// ReInitVM 完成后触发的事件 + /// + event Action OnAfterReInit; + + #endregion + + #region Method + /// + /// 调用 InitVM 并触发 OnAfterInit 事件 + /// + void DoInit(); + + /// + /// 调用 ReInitVM 并触发 OnAfterReInit 事件 + /// + void DoReInit(); + #endregion + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/IDataContext.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/IDataContext.cs new file mode 100644 index 0000000..59c7509 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/IDataContext.cs @@ -0,0 +1,143 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// IDataContext + /// + public interface IDataContext : IDisposable + { + /// + /// IsFake + /// + bool IsFake { get; set; } + + bool IsDebug { get; set; } + + DBTypeEnum DBType { get; set; } + /// + /// AddEntity + /// + /// + /// + void AddEntity(T entity) where T : TopBasePoco; + + /// + /// UpdateEntity + /// + void UpdateEntity(T entity) where T : TopBasePoco; + + /// + /// UpdateProperty + /// + void UpdateProperty(T entity, Expression> fieldExp) where T : TopBasePoco; + + /// + /// UpdateProperty + /// + void UpdateProperty(T entity, string fieldName) where T : TopBasePoco; + + /// + /// DeleteEntity + /// + void DeleteEntity(T entity) where T : TopBasePoco; + + /// + /// CascadeDelete + /// + void CascadeDelete(T entity) where T : TreePoco; + + /// + /// Set + /// + /// + /// + DbSet Set() where T : class; + + /// + /// Model + /// + IModel Model { get; } + + /// + /// Database + /// + DatabaseFacade Database { get; } + + /// + /// CSName + /// + string CSName { get; set; } + + #region SaveChange + + /// + /// SaveChanges + /// + /// + int SaveChanges(); + + /// + /// SaveChanges + /// + /// + int SaveChanges(bool acceptAllChangesOnSuccess); + + /// + /// SaveChangesAsync + /// + /// + Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// SaveChangesAsync + /// + /// + Task SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)); + + #endregion + + /// + /// 初始化 + /// + /// + /// + /// 返回true即数据新建完成,进入初始化操作,返回false即数据库已经存在 + Task DataInit(object AllModel, bool IsSpa); + + IDataContext CreateNew(); + IDataContext ReCreate(); + + /// + /// 执行存储过程,返回datatable + /// + /// 存储过程名称 + /// 参数 + /// + DataTable RunSP(string command, params object[] paras); + IEnumerable RunSP(string command, params object[] paras); + + /// + /// 执行sql语句,返回datatable + /// + /// 查询sql语句 + /// 参数 + /// + DataTable RunSQL(string command, params object[] paras); + IEnumerable RunSQL(string sql, params object[] paras); + DataTable Run(string sql, CommandType commandType, params object[] paras); + IEnumerable Run(string sql, CommandType commandType, params object[] paras); + object CreateCommandParameter(string name, object value, ParameterDirection dir); + + void SetLoggerFactory(ILoggerFactory factory); + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ISessionService.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ISessionService.cs new file mode 100644 index 0000000..1beb714 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/ISessionService.cs @@ -0,0 +1,12 @@ +namespace WalkingTec.Mvvm.Core +{ + /// + /// Session接口 + /// + public interface ISessionService + { + T Get(string key); + void Set(string key, T val); + string SessionId { get; } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/IUIService.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/IUIService.cs new file mode 100644 index 0000000..e8dfded --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/IUIService.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace WalkingTec.Mvvm.Core +{ + public interface IUIService + { + string MakeButton(ButtonTypesEnum buttonType, string url, string buttonText, int? width, int? height, string title = null, string buttonID = null,bool resizable = true, bool max = false, string currentdivid = "", string buttonClass = null, string style = null, RedirectTypesEnum rtype = RedirectTypesEnum.Layer); + string MakeDialogButton(ButtonTypesEnum buttonType, string url, string buttonText, int? width, int? height, string title = null, string buttonID = null, bool showDialog = true, bool resizable = true, bool max = false,string buttonClass = null, string style = null); + + string MakeDownloadButton(ButtonTypesEnum buttonType, Guid fileID, string buttonText = null, string _DONOT_USE_CS = "default", string buttonClass = null, string style = null); + + string MakeViewButton(ButtonTypesEnum buttonType, Guid fileID, string buttonText = null, int? width = null, int? height = null, string title = null, bool resizable = true, string _DONOT_USE_CS = "default", bool maxed = false, string buttonClass = null, string style = null); + + string MakeScriptButton(ButtonTypesEnum buttonType, string buttonText, string script = "", string buttonID = null, string url = null, string buttonClass = null, string style = null); + + string MakeCheckBox(bool ischeck, string text = null, string name = null, string value = null, bool isReadOnly = false); + + string MakeRadio(bool ischeck, string text = null, string name = null, string value = null, bool isReadOnly = false); + + string MakeCombo(string name = null, List value = null, string selectedValue = null, string emptyText = null, bool isReadOnly = false); + + string MakeTextBox(string name = null, string value = null, string emptyText = null, bool isReadOnly = false); + string MakeDateTime(string name = null, string value = null, string emptyText = null, bool isReadOnly = false); + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Implement/DefaultUIService.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Implement/DefaultUIService.cs new file mode 100644 index 0000000..33daaf7 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Implement/DefaultUIService.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace WalkingTec.Mvvm.Core.Implement +{ + public class DefaultUIService : IUIService + { + public string MakeDialogButton(ButtonTypesEnum buttonType, string url, string buttonText, int? width, int? height, string title = null, string buttonID = null, bool showDialog = true, bool resizable = true, bool max = false, string buttonClass = null, string style = null) + { + return ""; + } + + public string MakeDownloadButton(ButtonTypesEnum buttonType, Guid fileID, string buttonText = null, string _DONOT_USE_CS = "default", string buttonClass = null, string style = null) + { + return ""; + } + + public string MakeCheckBox(bool ischeck, string text = null, string name = null, string value = null, bool isReadOnly = false) + { + return ""; + } + + public string MakeButton(ButtonTypesEnum buttonType, string url, string buttonText, int? width, int? height, string title = null, string buttonID = null, bool resizable = true, bool max = false, string currentdivid = "", string buttonClass = null, string style = null, RedirectTypesEnum rtype = RedirectTypesEnum.Layer) + { + return ""; + } + + public string MakeViewButton(ButtonTypesEnum buttonType, Guid fileID, string buttonText = null, int? width = null, int? height = null, string title = null, bool resizable = true, string _DONOT_USE_CS = "default", bool maxed = false, string buttonClass = null, string style = null) + { + return ""; + } + + public string MakeScriptButton(ButtonTypesEnum buttonType, string buttonText, string script = "", string buttonID = null, string url = null, string buttonClass = null, string style = null) + { + return ""; + } + + public string MakeRadio(bool ischeck, string text = null, string name = null, string value = null, bool isReadOnly = false) + { + return ""; + } + + public string MakeCombo(string name = null, List value = null, string selectedValue = null, string emptyText = null, bool isReadOnly = false) + { + return ""; + } + + public string MakeTextBox(string name = null, string value = null, string emptyText = null, bool isReadOnly = false) + { + return ""; + } + + public string MakeDateTime(string name = null, string value = null, string emptyText = null, bool isReadOnly = false) + { + return ""; + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/BodyConverter.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/BodyConverter.cs new file mode 100644 index 0000000..97815c7 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/BodyConverter.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using WalkingTec.Mvvm.Core.Extensions; + +namespace WalkingTec.Mvvm.Core.Json +{ + /// + /// StringIgnoreLTGTConvert + /// 忽略客户端提交的 <及>字符 + /// + public class BodyConverter : JsonConverter + { + public override PostedBody Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var rv = new PostedBody(); + rv.ProNames = new List(); + List prefix = new List(); + int depth = 0; + string lastObjecName = ""; + int insideArray = 0; + JsonTokenType lastToken = JsonTokenType.Null; + while (true) + { + if (reader.TokenType == JsonTokenType.StartArray) + { + insideArray++; + depth++; + prefix.Add(lastObjecName + "[0]"); + } + if (reader.TokenType == JsonTokenType.EndArray) + { + insideArray--; + depth--; + prefix.RemoveAt(prefix.Count - 1); + } + if (reader.TokenType == JsonTokenType.StartObject) + { + if (insideArray == 0) + { + depth++; + prefix.Add(lastObjecName); + if (rv.ProNames.Count > 0) + { + rv.ProNames.RemoveAt(rv.ProNames.Count - 1); + } + } + else + { + if (lastToken != JsonTokenType.StartArray) + { + reader.TrySkip(); + reader.Read(); + continue; + } + } + } + if (reader.TokenType == JsonTokenType.PropertyName) + { + var pname = reader.GetString(); + lastObjecName = pname; + var p = prefix.Take(depth).ToSepratedString(seperator: "."); + if (string.IsNullOrEmpty(p) == false) + { + pname = p + "." + pname; + } + if (rv.ProNames.Contains(pname) == false) + { + rv.ProNames.Add(pname); + } + } + if (reader.TokenType == JsonTokenType.EndObject) + { + if (insideArray == 0) + { + depth--; + prefix.RemoveAt(prefix.Count - 1); + } + if (reader.IsFinalBlock == true && reader.CurrentDepth == 0) + { + reader.Read(); + break; + } + } + lastToken = reader.TokenType; + reader.Read(); + } + return rv; + } + + + public override void Write(Utf8JsonWriter writer, PostedBody value, JsonSerializerOptions options) + { + return; + } + + } + + public class PostedBody + { + public List ProNames { get; set; } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/BoolStringConverter.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/BoolStringConverter.cs new file mode 100644 index 0000000..ad00b3e --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/BoolStringConverter.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Unicode; +using NPOI.SS.Formula.Functions; +using WalkingTec.Mvvm.Core.Extensions; + +namespace WalkingTec.Mvvm.Core.Json +{ + public class BoolStringConverter : +JsonConverter + { + + public override bool Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options) + { + + JsonTokenType token = reader.TokenType; + + if (token == JsonTokenType.String) + { + var s = reader.GetString() ?? ""; + if (s.ToLower() == "true") + { + return true; + } + else if (s.ToLower() == "false") + { + return false; + } + else + { + return false; + } + } + else if (token == JsonTokenType.True || token == JsonTokenType.False) + { + return reader.GetBoolean(); + } + else + { + return false; + } + } + + public override void Write( + Utf8JsonWriter writer, + bool data, + JsonSerializerOptions options) + { + writer.WriteBooleanValue(data); + + } + + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/DateRangeConverter.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/DateRangeConverter.cs new file mode 100644 index 0000000..4bafe8b --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/DateRangeConverter.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using WalkingTec.Mvvm.Core; + +namespace WalkingTec.Mvvm.Core.Json +{ + public class DateRangeConverter : JsonConverter + { + public override DateRange Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + try + { + if (reader.TokenType == JsonTokenType.StartArray) + { + reader.Read(); + string[] ds = new string[2]; + ds[0] = reader.GetString(); + reader.Read(); + ds[1] = reader.GetString(); + reader.Read(); + if (DateRange.TryParse(ds, out var dateRange)) + { + return dateRange; + } + else + { + return null; + } + } + } + catch (Exception) + { + } + return null; + } + + public override void Write(Utf8JsonWriter writer, DateRange value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteNullValue(); + } + else + { + //writer.WriteStringValue(JsonSerializer.Serialize(value),); + writer.WriteStartArray(); + writer.WriteStringValue(value.GetStartTime().ToString()); + writer.WriteStringValue(value.GetEndTime().ToString()); + writer.WriteEndArray(); + } + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/DateTimeConverter.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/DateTimeConverter.cs new file mode 100644 index 0000000..74628d4 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/DateTimeConverter.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace WalkingTec.Mvvm.Core.Json +{ + + public class DateTimeConverter : JsonConverter + { + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + if (DateTime.TryParse(reader.GetString(), out DateTime date)) + return date; + } + return reader.GetDateTime(); + } + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString("yyyy-MM-dd HH:mm:ss")); + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/DynamicDataConverter.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/DynamicDataConverter.cs new file mode 100644 index 0000000..921c8a1 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/DynamicDataConverter.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using WalkingTec.Mvvm.Core.Extensions; + +namespace WalkingTec.Mvvm.Core.Json +{ + public class DynamicDataConverter : JsonConverter + { + public override DynamicData Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + try + { + DynamicData rv = new DynamicData(); + rv.Fields = new Dictionary(); + string currentkey = ""; + //object currentvalue; + int level = 0; + while (true) + { + if(reader.TokenType == JsonTokenType.StartObject) { + if(level > 0) + { + var inner = JsonSerializer.Deserialize(ref reader, options); + rv.Fields.Add(currentkey, inner); + } + level++; + } + if(reader.TokenType == JsonTokenType.EndObject) + { + level--; + } + if (reader.TokenType == JsonTokenType.PropertyName) + { + currentkey = reader.GetString(); + } + if(reader.TokenType == JsonTokenType.String || reader.TokenType == JsonTokenType.Number || reader.TokenType == JsonTokenType.True || reader.TokenType == JsonTokenType.False || reader.TokenType == JsonTokenType.Null) + { + var val = JsonSerializer.Deserialize(ref reader,options); + rv.Fields.Add(currentkey, val); + } + if (reader.IsFinalBlock && level == 0) + { + //reader.Read(); + break; + } + reader.Read(); + } + return rv; + } + catch + { + return null; + } + } + + public override void Write(Utf8JsonWriter writer, DynamicData value, JsonSerializerOptions options) + { + if (value == null || value.Fields == null) + { + writer.WriteNullValue(); + } + else + { + writer.WriteStartObject(); + foreach (var item in value.Fields) + { + if(item.Value == null) + { + if (options.IgnoreNullValues == false) + { + writer.WriteNull(item.Key); + } + } + else + { + writer.WritePropertyName(item.Key); + JsonSerializer.Serialize(writer, item.Value, options); + } + } + writer.WriteEndObject(); + } + } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/PocoConverter.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/PocoConverter.cs new file mode 100644 index 0000000..3697017 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/PocoConverter.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using System.Text.Unicode; +using System.Threading.Tasks; +using WalkingTec.Mvvm.Core.Extensions; + +namespace WalkingTec.Mvvm.Core.Json +{ + public class PocoConverter : JsonConverterFactory + { + + public override bool CanConvert(Type typeToConvert) + { + return typeof(TopBasePoco).IsAssignableFrom(typeToConvert); + } + + public override JsonConverter CreateConverter( + Type type, + JsonSerializerOptions options) + { + + var temp = CloneOptions(options); + foreach (var item in options.Converters) + { + if(item.GetType() != typeof(PocoConverter)) + { + temp.Converters.Add(item); + } + } + JsonConverter converter = (JsonConverter)Activator.CreateInstance( + typeof(PocoConverterInner<>).MakeGenericType( + new Type[] { type }), + BindingFlags.Instance | BindingFlags.Public, + binder: null, + args: new object[] { temp }, + culture: null); + + return converter; + } + + private JsonSerializerOptions CloneOptions(JsonSerializerOptions op) + { + JsonSerializerOptions rv = new JsonSerializerOptions(); + rv.PropertyNamingPolicy = op.PropertyNamingPolicy; + rv.AllowTrailingCommas = op.AllowTrailingCommas; + rv.DefaultBufferSize = op.DefaultBufferSize; + rv.DefaultIgnoreCondition = op.DefaultIgnoreCondition; + rv.DictionaryKeyPolicy = op.DictionaryKeyPolicy; + rv.Encoder = op.Encoder; + rv.IgnoreNullValues = op.IgnoreNullValues; + rv.IgnoreReadOnlyFields = op.IgnoreReadOnlyFields; + rv.IgnoreReadOnlyProperties = op.IgnoreReadOnlyProperties; + rv.IncludeFields = op.IncludeFields; + rv.DefaultIgnoreCondition = op.DefaultIgnoreCondition; + rv.DictionaryKeyPolicy = op.DictionaryKeyPolicy; + rv.MaxDepth = op.MaxDepth; + rv.NumberHandling = op.NumberHandling; + rv.ReadCommentHandling = op.ReadCommentHandling; + rv.ReferenceHandler = op.ReferenceHandler; + rv.WriteIndented = op.WriteIndented; + return rv; + } + + private class PocoConverterInner : + JsonConverter where T : TopBasePoco + { + protected readonly JsonSerializerOptions _options; + public PocoConverterInner(JsonSerializerOptions options) + { + _options = options; + } + + public override T Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options) + { + + return JsonSerializer.Deserialize(ref reader, _options); + } + + public override void Write( + Utf8JsonWriter writer, + T data, + JsonSerializerOptions options) + { + var _datacache = new Dictionary(); + RemoveCycleReference(data, _datacache); + JsonSerializer.Serialize(writer, data, typeof(T), _options); + } + + private void RemoveCycleReference(object Entity, Dictionary datacache) + { + var pros = Entity.GetType().GetAllProperties(); + var mainkey = Entity.GetType().FullName + (Entity as TopBasePoco).GetID(); + datacache.TryAdd(mainkey, 1); + + foreach (var pro in pros) + { + if (typeof(TopBasePoco).IsAssignableFrom(pro.PropertyType)) + { + var subentity = pro.GetValue(Entity) as TopBasePoco; + string key = pro.PropertyType.FullName + subentity?.GetID() ?? ""; + if (subentity != null && datacache.ContainsKey(key) == false) + { + RemoveCycleReference(subentity, datacache); + } + else + { + pro.SetValue(Entity,null); + } + } + //找到类型为List的字段 + if (pro.PropertyType.GenericTypeArguments.Count() > 0) + { + //获取xxx的类型 + var ftype = pro.PropertyType.GenericTypeArguments.First(); + //如果xxx继承自TopBasePoco + if (ftype.IsSubclassOf(typeof(TopBasePoco))) + { + //界面传过来的子表数据 + + if (pro.GetValue(Entity) is IEnumerable list && list.Count() > 0) + { + bool found = false; + foreach (var newitem in list) + { + if (newitem != null) + { + string subkey = ftype.FullName + newitem?.GetID() ?? ""; + if (datacache.ContainsKey(subkey) == false) + { + RemoveCycleReference(newitem, datacache.ToDictionary(x=>x.Key,x=>x.Value)); + found = true; + } + else + { + found = false; + break; + } + } + } + if(found == false) + { + pro.SetValue(Entity, null); + } + } + } + } + } + + } + } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/RawStringConverter.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/RawStringConverter.cs new file mode 100644 index 0000000..fa9a3f1 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/RawStringConverter.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Unicode; +using NPOI.SS.Formula.Functions; +using WalkingTec.Mvvm.Core.Extensions; + +namespace WalkingTec.Mvvm.Core.Json +{ + public class RawStringConverter : JsonConverter + { + public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + try + { + if (reader.TokenType == JsonTokenType.String) + { + string rv = reader.GetString(); + return rv; + } + } + catch (Exception) + { + } + return null; + } + + public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteNullValue(); + } + else + { + var txt = JsonEncodedText.Encode($"_raw_{value}_raw_", JavaScriptEncoder.UnsafeRelaxedJsonEscaping); + writer.WriteStringValue(txt); + } + } + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/StringConverter.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/StringConverter.cs new file mode 100644 index 0000000..3fb412e --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/StringConverter.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace System.Text.Json.Serialization +{ + public class JsonStringConverter : JsonConverter + { + public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + if (reader.TokenType == JsonTokenType.String) + { + return reader.GetString(); + } + return null; + } + + public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteNullValue(); + } + else + { + writer.WriteStringValue(value); + } + } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/StringIgnoreLTGTConverter.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/StringIgnoreLTGTConverter.cs new file mode 100644 index 0000000..ef3cedf --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/StringIgnoreLTGTConverter.cs @@ -0,0 +1,40 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace System.Text.Json.Serialization +{ + /// + /// StringIgnoreLTGTConvert + /// 忽略客户端提交的 <及>字符 + /// + public class StringIgnoreLTGTConverter : JsonConverter + { + public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + if (reader.TokenType == JsonTokenType.String) + { + return reader.GetString().Replace("<", string.Empty).Replace(">", string.Empty); + } + + return null; + } + + public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteNullValue(); + } + else + { + writer.WriteStringValue(value); + } + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/TypeConverter.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/TypeConverter.cs new file mode 100644 index 0000000..3cd77a1 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Json/TypeConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Unicode; +using NPOI.SS.Formula.Functions; +using WalkingTec.Mvvm.Core.Extensions; + +namespace WalkingTec.Mvvm.Core.Json +{ + public class TypeConverter : JsonConverter + { + public override Type Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return null; + } + + public override void Write(Utf8JsonWriter writer, Type value, JsonSerializerOptions options) + { + writer.WriteNullValue(); + } + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/JsonResultModel.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/JsonResultModel.cs new file mode 100644 index 0000000..550ec91 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/JsonResultModel.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// DataTableResult + /// + public class DataTableResult : JsonResultT> + where T : TopBasePoco + { + /// + /// Data Count + /// + public long Count { get; set; } + } + + /// + /// JsonResultT + /// + public class JsonResultT + { + /// + /// Status Code + /// + /// + public int Code { get; set; } + + /// + /// Message + /// + public string Msg { get; set; } + + /// + /// Data + /// + public T Data { get; set; } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/LoginUserInfo.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/LoginUserInfo.cs new file mode 100644 index 0000000..0a3d0bb --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/LoginUserInfo.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using NPOI.SS.Formula.Functions; +using WalkingTec.Mvvm.Core.Support.Json; + +namespace WalkingTec.Mvvm.Core +{ + + /// + /// 用户登录信息,需要保存在Session中,所以使用Serializable标记 + /// + public class LoginUserInfo + { + public string UserId { get; set; } + + /// + /// 登录用户 + /// + public string ITCode { get; set; } + + public string TenantCode { get; set; } + + public string Name { get; set; } + + public string Memo { get; set; } + + public Guid? PhotoId { get; set; } + + public List Roles { get; set; } + + public List Groups { get; set; } + + public Dictionary Attributes { get; set; } + /// + /// 用户的页面权限列表 + /// + public List FunctionPrivileges { get; set; } + /// + /// 用户的数据权限列表 + /// + public List DataPrivileges { get; set; } + + public async System.Threading.Tasks.Task LoadBasicInfoAsync(WTMContext context) + { + if (string.IsNullOrEmpty(this.ITCode) || context?.DC == null || context.BaseUserQuery == null) + { + return; + } + var DC = context.DC; + var userInfo = await context.BaseUserQuery + .Where(x => x.ITCode.ToLower() == this.ITCode.ToLower() && x.IsValid) + .Select(x => new { + user = x, + UserRoles = DC.Set().Where(y => y.UserCode == x.ITCode).ToList(), + UserGroups = DC.Set().Where(y => y.UserCode == x.ITCode).ToList(), + }) + .FirstOrDefaultAsync(); + + if (userInfo != null) + { + // 初始化用户信息 + var roleIDs = userInfo.UserRoles.Select(x => x.RoleCode).ToList(); + var groupIDs = userInfo.UserGroups.Select(x => x.GroupCode).ToList(); + + + var dataPris = await DC.Set().AsNoTracking() + .Where(x => x.UserCode == userInfo.user.ITCode || (x.GroupCode != null && groupIDs.Contains(x.GroupCode))) + .Distinct() + .ToListAsync(); + ProcessTreeDp(dataPris,context); + + //查找登录用户的页面权限 + var funcPrivileges = await DC.Set().AsNoTracking() + .Where(x => x.RoleCode != null && roleIDs.Contains(x.RoleCode)) + .Distinct() + .ToListAsync(); + + var roles = DC.Set().AsNoTracking().Where(x => roleIDs.Contains(x.RoleCode)).ToList(); + var groups = DC.Set().AsNoTracking().Where(x => groupIDs.Contains(x.GroupCode)).ToList(); + this.ITCode = userInfo.user.ITCode; + if (string.IsNullOrEmpty(this.Name)) + { + this.Name = userInfo.user.Name; + } + if (this.PhotoId == null) + { + this.PhotoId = userInfo.user.PhotoId; + } + if (string.IsNullOrEmpty(this.TenantCode)) + { + this.TenantCode = userInfo.user.TenantCode; + } + this.Roles = roles.Select(x => new SimpleRole { ID = x.ID, RoleCode = x.RoleCode, RoleName = x.RoleName }).ToList(); + this.Groups = groups.Select(x => new SimpleGroup { ID = x.ID, GroupCode = x.GroupCode, GroupName = x.GroupName }).ToList(); + this.DataPrivileges = dataPris.Select(x => new SimpleDataPri { ID = x.ID, RelateId = x.RelateId, TableName = x.TableName, UserCode = x.UserCode, GroupCode = x.GroupCode }).ToList(); + this.FunctionPrivileges = funcPrivileges.Select(x => new SimpleFunctionPri { ID = x.ID, RoleCode = x.RoleCode, Allowed = x.Allowed, MenuItemId = x.MenuItemId }).ToList(); + } + } + + private void ProcessTreeDp(List dps, WTMContext context) + { + var dpsSetting = context.DataPrivilegeSettings; + foreach (var dp in dpsSetting) + { + if (typeof(TreePoco).IsAssignableFrom(dp.ModelType)) + { + var ids = dps.Where(x => x.TableName == dp.ModelName).Select(x => x.RelateId).ToList(); + if (ids.Count > 0 && ids.Contains(null) == false) + { + var skipids = dp.GetTreeParentIds(context, dps); + List subids = new List(); + subids.AddRange(GetSubIds(dp, ids, dp.ModelType, skipids,context)); + subids = subids.Distinct().ToList(); + subids.ForEach(x => dps.Add(new DataPrivilege + { + TableName = dp.ModelName, + RelateId = x.ToString() + })); + } + } + + } + } + private IEnumerable GetSubIds(IDataPrivilege dp, List p_id, Type modelType, List skipids,WTMContext context) + { + var ids = p_id.Where(x => skipids.Contains(x) == false).ToList(); + var subids = dp.GetTreeSubIds(context, ids); + if (subids.Count > 0) + { + return subids.Concat(GetSubIds(dp, subids, modelType, skipids,context)); + } + else + { + return new List(); + } + } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/MSD.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/MSD.cs new file mode 100644 index 0000000..56ed5c4 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/MSD.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// 模型状态接口 + /// + public interface IModelStateService + { + /// + /// 索引 + /// + /// + /// + List this[string name] { get; } + /// + /// 添加模型错误 + /// + /// 字段名称 + /// 错误信息 + void AddModelError(string key, string errorMessage); + void RemoveModelError(string key); + int Count { get; } + + IEnumerable Keys { get; } + + void Clear(); + + string GetFirstError(); + + bool IsValid { get; } + } + + + + /// + /// 记录错误的简单类 + /// + public class MsdError + { + public string ErrorMessage { get; set; } + public Exception Exception { get; set; } + } + + + public class BasicMSD : IModelStateService + { + private Dictionary _states; + + public BasicMSD() + { + this._states = new Dictionary(); + } + + public List this[string name] + { + get + { + return _states.Where(x => x.Key == name).Select(x => new MsdError { ErrorMessage = x.Value }).ToList(); + } + } + + /// + /// 添加错误信息 + /// + /// 错误的字段名 + /// 错误信息 + public void AddModelError(string key, string errorMessage) + { + _states.Add(key, errorMessage); + } + + public void RemoveModelError(string key) + { + _states.Remove(key); + } + + public void Clear() + { + _states.Clear(); + } + + public string GetFirstError() + { + string rv = ""; + foreach (var key in Keys) + { + if (this[key].Count > 0) + { + rv = this[key].First().ErrorMessage; + } + } + return rv; + } + + public int Count => _states.Count; + + public IEnumerable Keys => _states.Keys; + + bool IModelStateService.IsValid => _states.Count > 0 ? false : true; + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/ActionLog.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/ActionLog.cs new file mode 100644 index 0000000..bf4433a --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/ActionLog.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace WalkingTec.Mvvm.Core +{ + public enum ActionLogTypesEnum + { + [Display(Name = "_Admin.Normal")] + Normal, + [Display(Name = "_Admin.Exception")] + Exception, + [Display(Name = "_Admin.Debug")] + Debug + }; + + /// + /// ActionLog + /// + [Table("ActionLogs")] + public class ActionLog : BasePoco, ICloneable + { + [Display(Name = "_Admin.Module")] + [StringLength(255, ErrorMessage = "Validate.{0}stringmax{1}")] + public string ModuleName { get; set; } + + [Display(Name = "_Admin.Action")] + [StringLength(255, ErrorMessage = "Validate.{0}stringmax{1}")] + public string ActionName { get; set; } + + [Display(Name = "_Admin.Account")] + [StringLength(50, ErrorMessage = "Validate.{0}stringmax{1}")] + public string ITCode { get; set; } + + [Display(Name = "Url")] + [StringLength(250, ErrorMessage = "Validate.{0}stringmax{1}")] + public string ActionUrl { get; set; } + + [Display(Name = "_Admin.ActionTime")] + public DateTime ActionTime { get; set; } + + [Display(Name = "_Admin.Duration")] + public double Duration { get; set; } + + [Display(Name = "_Admin.Remark")] + public string Remark { get; set; } + + [StringLength(50, ErrorMessage = "Validate.{0}stringmax{1}")] + [Display(Name = "IP")] + public string IP { get; set; } + + [Display(Name = "_Admin.LogType")] + public ActionLogTypesEnum LogType { get; set; } + + public object Clone() + { + return this.MemberwiseClone(); + } + + public string GetLogString() + { + return $@" +|-{Core.CoreProgram._localizer?["_Admin.ActionTime"]}:{this.ActionTime} +|-{Core.CoreProgram._localizer?["_Admin.Account"]}:{this.ITCode??""} +|-IP:{this.IP??""} +|-{Core.CoreProgram._localizer?["_Admin.Module"]}:{this.ModuleName??""} +|-{Core.CoreProgram._localizer?["_Admin.MethodName"]}:{this.ActionName ?? ""} +|-Url:{this.ActionUrl ?? ""} +|-{Core.CoreProgram._localizer?["_Admin.Duration"]}:{this.Duration.ToString("F2")+" s"} +|-{Core.CoreProgram._localizer?["_Admin.Remark"]}:{this.Remark} +"; + } + } + + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/BasePoco.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/BasePoco.cs new file mode 100644 index 0000000..4562706 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/BasePoco.cs @@ -0,0 +1,44 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace WalkingTec.Mvvm.Core +{ + + public interface IBasePoco + { + DateTime? CreateTime { get; set; } + string CreateBy { get; set; } + DateTime? UpdateTime { get; set; } + string UpdateBy { get; set; } + } + + /// + /// Model层的基类,所有的model都应该继承这个类。这会使所有的model层对应的数据库表都有一个自增主键 + /// + public class BasePoco : TopBasePoco, IBasePoco + { + /// + /// CreateTime + /// + [Display(Name = "_Admin.CreateTime")] + public DateTime? CreateTime { get; set; } + /// + /// CreateBy + /// + [Display(Name = "_Admin.CreateBy")] + [StringLength(50,ErrorMessage = "Validate.{0}stringmax{1}")] + public string CreateBy { get; set; } + /// + /// UpdateTime + /// + [Display(Name = "_Admin.UpdateTime")] + public DateTime? UpdateTime { get; set; } + /// + /// UpdateBy + /// + [Display(Name = "_Admin.UpdateBy")] + [StringLength(50,ErrorMessage = "Validate.{0}stringmax{1}")] + public string UpdateBy { get; set; } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/DataPrivilege.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/DataPrivilege.cs new file mode 100644 index 0000000..898bf3e --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/DataPrivilege.cs @@ -0,0 +1,30 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// DataPrivilege + /// + [Table("DataPrivileges")] + public class DataPrivilege : BasePoco + { + [Display(Name = "_Admin.User")] + public string UserCode { get; set; } + + [Display(Name = "_Admin.Group")] + public string GroupCode { get; set; } + + [Required(ErrorMessage = "Validate.{0}required")] + [StringLength(50,ErrorMessage = "Validate.{0}stringmax{1}")] + [Display(Name = "_Admin.TableName")] + public string TableName { get; set; } + public string RelateId { get; set; } + + [Display(Name = "_Admin.Domain")] + public string Domain { get; set; } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FileAttachment.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FileAttachment.cs new file mode 100644 index 0000000..189a5f7 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FileAttachment.cs @@ -0,0 +1,60 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.IO; +using System.Text.Json.Serialization; +using WalkingTec.Mvvm.Core.Models; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// FileAttachment + /// + [Table("FileAttachments")] + public class FileAttachment : TopBasePoco, IWtmFile, IDisposable + { + [Display(Name = "_Admin.FileName")] + [Required(ErrorMessage = "Validate.{0}required")] + public string FileName { get; set; } + + [Display(Name = "_Admin.FileExt")] + [Required(ErrorMessage = "Validate.{0}required")] + [StringLength(10)] + public string FileExt { get; set; } + + [Display(Name = "_Admin.Path")] + public string Path { get; set; } + + [Display(Name = "_Admin.Length")] + public long Length { get; set; } + + public DateTime UploadTime { get; set; } + + public string SaveMode { get; set; } + + public byte[] FileData { get; set; } + + public string ExtraInfo { get; set; } + public string HandlerInfo { get; set; } + + + [NotMapped] + [JsonIgnore] + public Stream DataStream { get; set; } + + public void Dispose() + { + if(DataStream != null) + { + DataStream.Dispose(); + } + } + + string IWtmFile.GetID() + { + return ID.ToString(); + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FrameworkGroup.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FrameworkGroup.cs new file mode 100644 index 0000000..0d18d1d --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FrameworkGroup.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; +using System.Xml.Serialization; + +namespace WalkingTec.Mvvm.Core +{ + + [Table("FrameworkGroups")] + public class FrameworkGroup : BasePoco + { + [Display(Name = "_Admin.GroupCode")] + [Required(ErrorMessage = "Validate.{0}required")] + [RegularExpression("^[0-9]*$", ErrorMessage = "Validate.{0}number")] + [StringLength(100, ErrorMessage = "Validate.{0}stringmax{1}")] + public string GroupCode { get; set; } + + [Display(Name = "_Admin.GroupName")] + [StringLength(50, ErrorMessage = "Validate.{0}stringmax{1}")] + [Required(ErrorMessage = "Validate.{0}required")] + public string GroupName { get; set; } + + [Display(Name = "_Admin.Remark")] + public string GroupRemark { get; set; } + + [NotMapped] + [Display(Name = "_Admin.UsersCount")] + public int UsersCount { get; set; } + + [Display(Name = "_Admin.Tenant")] + public string TenantCode { get; set; } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FrameworkMenu.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FrameworkMenu.cs new file mode 100644 index 0000000..68010ed --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FrameworkMenu.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// FrameworkMenu + /// + [Table("FrameworkMenus")] + public class FrameworkMenu : TreePoco + { + + [Display(Name = "_Admin.PageName")] + [StringLength(50, ErrorMessage = "Validate.{0}stringmax{1}")] + [Required(ErrorMessage = "Validate.{0}required")] + public string PageName { get; set; } + + [Display(Name = "_Admin.ActionName")] + public string ActionName { get; set; } + + [Display(Name = "Codegen.ModuleName")] + public string ModuleName { get; set; } + + [Display(Name = "_Admin.FolderOnly")] + [Required(ErrorMessage = "Validate.{0}required")] + public bool FolderOnly { get; set; } + + [Display(Name = "_Admin.IsInherit")] + [Required(ErrorMessage = "Validate.{0}required")] + public bool IsInherit { get; set; } + + [Display(Name = "_Admin.Privileges")] + public List Privileges { get; set; } + + /// + /// ClassName + /// + /// + public string ClassName { get; set; } + + /// + /// MethodName + /// + /// + public string MethodName { get; set; } + + [Display(Name = "_Admin.Domain")] + public string Domain { get; set; } + + [Display(Name = "_Admin.ShowOnMenu")] + [Required(ErrorMessage = "Validate.{0}required")] + public bool ShowOnMenu { get; set; } + + [Display(Name = "_Admin.IsPublic")] + [Required(ErrorMessage = "Validate.{0}required")] + public bool IsPublic { get; set; } + + [Display(Name = "_Admin.DisplayOrder")] + [Required(ErrorMessage = "Validate.{0}required")] + public int? DisplayOrder { get; set; } + + [Display(Name = "_Admin.IsInside")] + [Required(ErrorMessage = "Validate.{0}required")] + public bool? IsInside { get; set; } + + /// + /// Url + /// + /// + public string Url { get; set; } + + [Display(Name = "_Admin.Icon")] + [StringLength(50)] + public string Icon { get; set; } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FrameworkRole.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FrameworkRole.cs new file mode 100644 index 0000000..e461b72 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FrameworkRole.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; +using System.Xml.Serialization; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// FrameworkRole + /// + [Table("FrameworkRoles")] + public class FrameworkRole : BasePoco + { + [Display(Name = "_Admin.RoleCode")] + [Required(ErrorMessage = "Validate.{0}required")] + [RegularExpression("^[0-9]*$", ErrorMessage = "Validate.{0}number")] + [StringLength(100, ErrorMessage = "Validate.{0}stringmax{1}")] + public string RoleCode { get; set; } + + [Display(Name = "_Admin.RoleName")] + [StringLength(50, ErrorMessage = "Validate.{0}stringmax{1}")] + [Required(ErrorMessage = "Validate.{0}required")] + public string RoleName { get; set; } + + [Display(Name = "_Admin.Remark")] + public string RoleRemark { get; set; } + + [Display(Name = "_Admin.Tenant")] + public string TenantCode { get; set; } + + + [NotMapped] + [Display(Name = "_Admin.UsersCount")] + public int UsersCount { get; set; } + + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FrameworkUser.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FrameworkUser.cs new file mode 100644 index 0000000..5885650 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FrameworkUser.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using System.Xml.Serialization; +using Microsoft.EntityFrameworkCore; +using WalkingTec.Mvvm.Core.Support.Json; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// FrameworkUser + /// + [Table("FrameworkUsers")] + public class FrameworkUserBase : BasePoco + { + [Display(Name = "_Admin.Account")] + [Required(ErrorMessage = "Validate.{0}required")] + [StringLength(50,ErrorMessage = "Validate.{0}stringmax{1}")] + public string ITCode { get; set; } + + [Display(Name = "_Admin.Password")] + [Required(AllowEmptyStrings = false, ErrorMessage = "Validate.{0}required")] + [StringLength(32, ErrorMessage = "Validate.{0}stringmax{1}")] + public string Password { get; set; } + + [Display(Name = "_Admin.Name")] + [Required(ErrorMessage = "Validate.{0}required")] + [StringLength(50, ErrorMessage = "Validate.{0}stringmax{1}")] + public string Name { get; set; } + + [Display(Name = "_Admin.IsValid")] + public bool IsValid { get; set; } + + [Display(Name = "_Admin.Photo")] + public Guid? PhotoId { get; set; } + + [Display(Name = "_Admin.Photo")] + [JsonIgnore] + public FileAttachment Photo { get; set; } + + [Display(Name = "_Admin.Tenant")] + public string TenantCode { get; set; } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FrameworkUserGroup.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FrameworkUserGroup.cs new file mode 100644 index 0000000..216e849 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FrameworkUserGroup.cs @@ -0,0 +1,19 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; +using WalkingTec.Mvvm.Core.Attributes; + +namespace WalkingTec.Mvvm.Core +{ + [Table("FrameworkUserGroups")] + public class FrameworkUserGroup : BasePoco + { + [Required] + public string UserCode { get; set; } + [Display(Name = "_Admin.Group")] + [Required] + public string GroupCode { get; set; } + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FrameworkUserRole.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FrameworkUserRole.cs new file mode 100644 index 0000000..7279b75 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FrameworkUserRole.cs @@ -0,0 +1,18 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; +using WalkingTec.Mvvm.Core.Attributes; + +namespace WalkingTec.Mvvm.Core +{ + [Table("FrameworkUserRoles")] + public class FrameworkUserRole : BasePoco + { + [Required] + public string UserCode { get; set; } + [Required] + [Display(Name = "_Admin.Role")] + public string RoleCode { get; set; } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FunctionPrivilege.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FunctionPrivilege.cs new file mode 100644 index 0000000..d542c71 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/FunctionPrivilege.cs @@ -0,0 +1,26 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// FunctionPrivilege + /// + [Table("FunctionPrivileges")] + public class FunctionPrivilege : BasePoco + { + [Display(Name = "_Admin.Role")] + public string RoleCode { get; set; } + + [Display(Name = "_Admin.MenuItem")] + public Guid MenuItemId { get; set; } + + [Display(Name = "_Admin.MenuItem")] + public FrameworkMenu MenuItem { get; set; } + + [Display(Name = "_Admin.Allowed")] + [Required(ErrorMessage = "Validate.{0}required")] + public bool? Allowed { get; set; } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/ISearcher.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/ISearcher.cs new file mode 100644 index 0000000..0293808 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/ISearcher.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using WalkingTec.Mvvm.Core; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// ISearcher + /// + public interface ISearcher + { + #region Property + + #region 分页相关 + /// + /// 当前页 + /// + int Page { get; set; } + /// + /// 每页数 + /// + int Limit { get; set; } + /// + /// 记录数 + /// + long Count { get; set; } + /// + /// 分页数 + /// + int PageCount { get; set; } + #endregion + + /// + /// 记录 Controller 中的表单数据 + /// + Dictionary FC { get; set; } + + IDataContext DC { get; set; } + /// + /// VMFullName + /// + string VMFullName { get; } + + ISessionService Session { get; } + + LoginUserInfo LoginUserInfo { get; } + /// + /// 排序信息 + /// + SortInfo SortInfo { get; set; } + /// + /// 用于框架判断列表页是否全局刷新 + /// + + #endregion + + #region Event + + /// + /// InitVM 完成后触发的事件 + /// + event Action OnAfterInit; + /// + /// ReInitVM 完成后触发的事件 + /// + event Action OnAfterReInit; + + #endregion + + #region Method + /// + /// 将源 VM 的 FC 等内容复制到本VM中 + /// + /// + void CopyContext(IBaseVM vm); + + /// + /// 调用 InitVM 并触发 OnAfterInit 事件 + /// + void DoInit(); + + /// + /// 调用 ReInitVM 并触发 OnAfterReInit 事件 + /// + void DoReInit(); + + #endregion + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/ISubFile.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/ISubFile.cs new file mode 100644 index 0000000..4aa27ac --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/ISubFile.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace WalkingTec.Mvvm.Core +{ + public interface ISubFile + { + Guid FileId { get; set; } + FileAttachment File { get; set; } + int Order { get; set; } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/IWtmFile.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/IWtmFile.cs new file mode 100644 index 0000000..f04d6f4 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/IWtmFile.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace WalkingTec.Mvvm.Core.Models +{ + public interface IWtmFile + { + string Path { get; set; } + + string FileName { get; set; } + string FileExt { get; set; } + long Length { get; set; } + + DateTime UploadTime { get; set; } + + string SaveMode { get; set; } + string ExtraInfo { get; set; } + string HandlerInfo { get; set; } + + Stream DataStream { get; set; } + + string GetID(); + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/PersistPoco.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/PersistPoco.cs new file mode 100644 index 0000000..6d8c17d --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/PersistPoco.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace WalkingTec.Mvvm.Core +{ + public interface IPersistPoco + { + bool IsValid { get; set; } + } + + /// + /// 所有持久化model的基类,所有的不应被物理删除的model都应该继承这个类 + /// + public abstract class PersistPoco : BasePoco, IPersistPoco + { + /// + /// IsValid + /// + [Display(Name = "_Admin.IsValid")] + public bool IsValid { get; set; } = true; + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/PersistedGrant.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/PersistedGrant.cs new file mode 100644 index 0000000..6b4b8a3 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/PersistedGrant.cs @@ -0,0 +1,34 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// PersistedGrant + /// + [Table("PersistedGrants")] + public class PersistedGrant : TopBasePoco + { + [StringLength(50)] + public string Type { get; set; } + + public string UserCode { get; set; } + + /// + /// 创建时间 + /// + /// The creation time. + public DateTime CreationTime { get; set; } = DateTime.Now; + + /// + /// 到期时间 + /// + /// The expiration. + public DateTime Expiration { get; set; } + + [StringLength(50)] + public string RefreshToken { get; set; } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/TopBasePoco.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/TopBasePoco.cs new file mode 100644 index 0000000..7a73aac --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/TopBasePoco.cs @@ -0,0 +1,116 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Reflection; +using System.Text.Json.Serialization; +using WalkingTec.Mvvm.Core.Extensions; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// TopBasePoco + /// + public class TopBasePoco + { + /// + /// Id + /// + [Key] + public Guid ID + { + get; set; + } + + /// + /// 是否选中 + /// 标识当前行数据是否被选中 + /// + [NotMapped] + //[JsonConverter(typeof(InternalBoolConverter))] + //[JsonProperty("LAY_CHECKED")] + [JsonIgnore] + public bool Checked { get; set; } + + /// + /// BatchError + /// + [NotMapped] + [JsonIgnore] + public string BatchError { get; set; } + + /// + /// ExcelIndex + /// + [NotMapped] + [JsonIgnore] + public long ExcelIndex { get; set; } + + public object GetID() + { + var idpro = this.GetType().GetSingleProperty("ID"); + var id = idpro.GetValue(this); + return id; + } + + public bool HasID() + { + bool rv = false; + var id = this.GetID(); + switch (id) + { + case Guid g1 when g1 != Guid.Empty: + rv = true; + break; + case string s when string.IsNullOrEmpty(s) == false: + rv = true; + break; + case int i when i>0: + rv = true; + break; + case long l when l > 0: + rv = true; + break; + } + return rv; + } + + public object GetParentID() + { + var idpro = this.GetType().GetSingleProperty("ParentId"); + var id = idpro.GetValue(this) ?? ""; + return id; + } + + + public Type GetIDType() + { + var idpro = this.GetType().GetSingleProperty("ID"); + return idpro.PropertyType; + } + + public void SetID(object id) + { + var idpro = this.GetType().GetSingleProperty("ID"); + idpro.SetValue(this, id.ConvertValue(idpro.PropertyType)); + + } + + private bool? _isBasePoco = null; + [NotMapped] + [JsonIgnore] + public bool IsBasePoco + { + get + { + if(_isBasePoco == null) + { + _isBasePoco = typeof(IBasePoco).IsAssignableFrom(this.GetType()); + } + return _isBasePoco.Value; + } + } + } + + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/TrackingObj.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/TrackingObj.cs new file mode 100644 index 0000000..59e2af7 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/TrackingObj.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Forms; + +namespace WalkingTec.Mvvm.Core.Models +{ + public class TrackingObj + { + public object Model { get; set; } + public List ChangedFields { get; set; } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/TreePoco.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/TreePoco.cs new file mode 100644 index 0000000..7175d9c --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Models/TreePoco.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace WalkingTec.Mvvm.Core +{ + public abstract class TreePoco : TopBasePoco + { + [Display(Name = "_Admin.Parent")] + public Guid? ParentId { get; set; } + + } + + public class TreePoco : TreePoco where T:TreePoco + { + + [Display(Name = "_Admin.Parent")] + [JsonIgnore] + public T Parent { get; set; } + [InverseProperty("Parent")] + [Display(Name = "_Admin.Children")] + public List Children { get; set; } + + [NotMapped] + public bool HasChildren + { + get + { + return Children?.Any() == true; + } + } + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/AuthConstants.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/AuthConstants.cs new file mode 100644 index 0000000..529fc51 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/AuthConstants.cs @@ -0,0 +1,314 @@ +namespace WalkingTec.Mvvm.Core.Auth +{ + public static class AuthConstants + { + public const string JwtTokenType = "Bearer"; + public const string AuthenticationType = "WTMAuthentication"; + public const string CookieAuthName = "WTM.CookieWithJwtAuth"; + + public static class JwtClaimTypes + { + // + // Summary: + // Unique Identifier for the End-User at the Issuer. + public const string Subject = "sub"; + // + // Summary: + // The iat (issued at) claim identifies the time at which the JWT was issued, , + // specified as the number of seconds from 1970-01-01T0:0:0Z + public const string IssuedAt = "iat"; + // + // Summary: + // Authentication Methods References. JSON array of strings that are identifiers + // for authentication methods used in the authentication. + public const string AuthenticationMethod = "amr"; + // + // Summary: + // Session identifier. This represents a Session of an OP at an RP to a User Agent + // or device for a logged-in End-User. Its contents are unique to the OP and opaque + // to the RP. + public const string SessionId = "sid"; + // + // Summary: + // Authentication Context Class Reference. String specifying an Authentication Context + // Class Reference value that identifies the Authentication Context Class that the + // authentication performed satisfied. The value "0" indicates the End-User authentication + // did not meet the requirements of ISO/IEC 29115 level 1. Authentication using + // a long-lived browser cookie, for instance, is one example where the use of "level + // 0" is appropriate. Authentications with level 0 SHOULD NOT be used to authorize + // access to any resource of any monetary value. (This corresponds to the OpenID + // 2.0 PAPE nist_auth_level 0.) An absolute URI or an RFC 6711 registered name SHOULD + // be used as the acr value; registered names MUST NOT be used with a different + // meaning than that which is registered. Parties using this claim will need to + // agree upon the meanings of the values used, which may be context-specific. The + // acr value is a case sensitive string. + public const string AuthenticationContextClassReference = "acr"; + // + // Summary: + // Time when the End-User authentication occurred. Its value is a JSON number representing + // the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time. + // When a max_age request is made or when auth_time is requested as an Essential + // Claim, then this Claim is REQUIRED; otherwise, its inclusion is OPTIONAL. + public const string AuthenticationTime = "auth_time"; + // + // Summary: + // The party to which the ID Token was issued. If present, it MUST contain the OAuth + // 2.0 Client ID of this party. This Claim is only needed when the ID Token has + // a single audience value and that audience is different than the authorized party. + // It MAY be included even when the authorized party is the same as the sole audience. + // The azp value is a case sensitive string containing a StringOrURI value. + public const string AuthorizedParty = "azp"; + // + // Summary: + // Access Token hash value. Its value is the base64url encoding of the left-most + // half of the hash of the octets of the ASCII representation of the access_token + // value, where the hash algorithm used is the hash algorithm used in the alg Header + // Parameter of the ID Token's JOSE Header. For instance, if the alg is RS256, hash + // the access_token value with SHA-256, then take the left-most 128 bits and base64url + // encode them. The at_hash value is a case sensitive string. + public const string AccessTokenHash = "at_hash"; + // + // Summary: + // Code hash value. Its value is the base64url encoding of the left-most half of + // the hash of the octets of the ASCII representation of the code value, where the + // hash algorithm used is the hash algorithm used in the alg Header Parameter of + // the ID Token's JOSE Header. For instance, if the alg is HS512, hash the code + // value with SHA-512, then take the left-most 256 bits and base64url encode them. + // The c_hash value is a case sensitive string. + public const string AuthorizationCodeHash = "c_hash"; + // + // Summary: + // Time the End-User's information was last updated. Its value is a JSON number + // representing the number of seconds from 1970-01-01T0:0:0Z as measured in UTC + // until the date/time. + public const string UpdatedAt = "updated_at"; + // + // Summary: + // String value used to associate a Client session with an ID Token, and to mitigate + // replay attacks. The value is passed through unmodified from the Authentication + // Request to the ID Token. If present in the ID Token, Clients MUST verify that + // the nonce Claim Value is equal to the value of the nonce parameter sent in the + // Authentication Request. If present in the Authentication Request, Authorization + // Servers MUST include a nonce Claim in the ID Token with the Claim Value being + // the nonce value sent in the Authentication Request. Authorization Servers SHOULD + // perform no other processing on nonce values used. The nonce value is a case sensitive + // string. + public const string Nonce = "nonce"; + // + // Summary: + // Defines a set of event statements that each may add additional claims to fully + // describe a single logical event that has occurred. + public const string Events = "events"; + // + // Summary: + // OAuth 2.0 Client Identifier valid at the Authorization Server. + public const string ClientId = "client_id"; + // + // Summary: + // OpenID Connect requests MUST contain the "openid" scope value. If the openid + // scope value is not present, the behavior is entirely unspecified. Other scope + // values MAY be present. Scope values used that are not understood by an implementation + // SHOULD be ignored. + public const string Scope = "scope"; + // + // Summary: + // The "act" (actor) claim provides a means within a JWT to express that delegation + // has occurred and identify the acting party to whom authority has been delegated.The + // "act" claim value is a JSON object and members in the JSON object are claims + // that identify the actor. The claims that make up the "act" claim identify and + // possibly provide additional information about the actor. + public const string Actor = "act"; + // + // Summary: + // The "may_act" claim makes a statement that one party is authorized to become + // the actor and act on behalf of another party. The claim value is a JSON object + // and members in the JSON object are claims that identify the party that is asserted + // as being eligible to act for the party identified by the JWT containing the claim. + public const string MayAct = "may_act"; + // + // Summary: + // an identifier + public const string Id = "id"; + // + // Summary: + // The identity provider + public const string IdentityProvider = "idp"; + // + // Summary: + // The role + public const string Role = "role"; + public const string TenantCode = "tenant"; + // + // Summary: + // JWT ID. A unique identifier for the token, which can be used to prevent reuse + // of the token. These tokens MUST only be used once, unless conditions for reuse + // were negotiated between the parties; any such negotiation is beyond the scope + // of this specification. + public const string JwtId = "jti"; + // + // Summary: + // The exp (expiration time) claim identifies the expiration time on or after which + // the token MUST NOT be accepted for processing, specified as the number of seconds + // from 1970-01-01T0:0:0Z + public const string Expiration = "exp"; + // + // Summary: + // The time before which the JWT MUST NOT be accepted for processing, specified + // as the number of seconds from 1970-01-01T0:0:0Z + public const string NotBefore = "nbf"; + // + // Summary: + // Issuer Identifier for the Issuer of the response. The iss value is a case sensitive + // URL using the https scheme that contains scheme, host, and optionally, port number + // and path components and no query or fragment components. + public const string Issuer = "iss"; + // + // Summary: + // End-User's full name in displayable form including all name parts, possibly including + // titles and suffixes, ordered according to the End-User's locale and preferences. + public const string Name = "name"; + // + // Summary: + // Given name(s) or first name(s) of the End-User. Note that in some cultures, people + // can have multiple given names; all can be present, with the names being separated + // by space characters. + public const string GivenName = "given_name"; + // + // Summary: + // Surname(s) or last name(s) of the End-User. Note that in some cultures, people + // can have multiple family names or no family name; all can be present, with the + // names being separated by space characters. + public const string FamilyName = "family_name"; + // + // Summary: + // Middle name(s) of the End-User. Note that in some cultures, people can have multiple + // middle names; all can be present, with the names being separated by space characters. + // Also note that in some cultures, middle names are not used. + public const string MiddleName = "middle_name"; + // + // Summary: + // Casual name of the End-User that may or may not be the same as the given_name. + // For instance, a nickname value of Mike might be returned alongside a given_name + // value of Michael. + public const string NickName = "nickname"; + // + // Summary: + // Shorthand name by which the End-User wishes to be referred to at the RP, such + // as janedoe or j.doe. This value MAY be any valid JSON string including special + // characters such as @, /, or whitespace. The relying party MUST NOT rely upon + // this value being unique + // + // Remarks: + // The RP MUST NOT rely upon this value being unique, as discussed in http://openid.net/specs/openid-connect-basic-1_0-32.html#ClaimStability + public const string PreferredUserName = "preferred_username"; + // + // Summary: + // URL of the End-User's profile page. The contents of this Web page SHOULD be about + // the End-User. + public const string Profile = "profile"; + // + // Summary: + // URL of the End-User's profile picture. This URL MUST refer to an image file (for + // example, a PNG, JPEG, or GIF image file), rather than to a Web page containing + // an image. + // + // Remarks: + // Note that this URL SHOULD specifically reference a profile photo of the End-User + // suitable for displaying when describing the End-User, rather than an arbitrary + // photo taken by the End-User. + public const string Picture = "picture"; + // + // Summary: + // URL of the End-User's Web page or blog. This Web page SHOULD contain information + // published by the End-User or an organization that the End-User is affiliated + // with. + public const string WebSite = "website"; + // + // Summary: + // End-User's preferred e-mail address. Its value MUST conform to the RFC 5322 [RFC5322] + // addr-spec syntax. The relying party MUST NOT rely upon this value being unique + public const string Email = "email"; + // + // Summary: + // "true" if the End-User's e-mail address has been verified; otherwise "false". + // + // Remarks: + // When this Claim Value is "true", this means that the OP took affirmative steps + // to ensure that this e-mail address was controlled by the End-User at the time + // the verification was performed. The means by which an e-mail address is verified + // is context-specific, and dependent upon the trust framework or contractual agreements + // within which the parties are operating. + public const string EmailVerified = "email_verified"; + // + // Summary: + // End-User's gender. Values defined by this specification are "female" and "male". + // Other values MAY be used when neither of the defined values are applicable. + public const string Gender = "gender"; + // + // Summary: + // End-User's birthday, represented as an ISO 8601:2004 [ISO8601‑2004] YYYY-MM-DD + // format. The year MAY be 0000, indicating that it is omitted. To represent only + // the year, YYYY format is allowed. Note that depending on the underlying platform's + // date related function, providing just year can result in varying month and day, + // so the implementers need to take this factor into account to correctly process + // the dates. + public const string BirthDate = "birthdate"; + // + // Summary: + // String from the time zone database (http://www.twinsun.com/tz/tz-link.htm) representing + // the End-User's time zone. For example, Europe/Paris or America/Los_Angeles. + public const string ZoneInfo = "zoneinfo"; + // + // Summary: + // End-User's locale, represented as a BCP47 [RFC5646] language tag. This is typically + // an ISO 639-1 Alpha-2 [ISO639‑1] language code in lowercase and an ISO 3166-1 + // Alpha-2 [ISO3166‑1] country code in uppercase, separated by a dash. For example, + // en-US or fr-CA. As a compatibility note, some implementations have used an underscore + // as the separator rather than a dash, for example, en_US; Relying Parties MAY + // choose to accept this locale syntax as well. + public const string Locale = "locale"; + // + // Summary: + // End-User's preferred telephone number. E.164 (https://www.itu.int/rec/T-REC-E.164/e) + // is RECOMMENDED as the format of this Claim, for example, +1 (425) 555-1212 or + // +56 (2) 687 2400. If the phone number contains an extension, it is RECOMMENDED + // that the extension be represented using the RFC 3966 [RFC3966] extension syntax, + // for example, +1 (604) 555-1234;ext=5678. + public const string PhoneNumber = "phone_number"; + // + // Summary: + // True if the End-User's phone number has been verified; otherwise false. When + // this Claim Value is true, this means that the OP took affirmative steps to ensure + // that this phone number was controlled by the End-User at the time the verification + // was performed. + // + // Remarks: + // The means by which a phone number is verified is context-specific, and dependent + // upon the trust framework or contractual agreements within which the parties are + // operating. When true, the phone_number Claim MUST be in E.164 format and any + // extensions MUST be represented in RFC 3966 format. + public const string PhoneNumberVerified = "phone_number_verified"; + // + // Summary: + // End-User's preferred postal address. The value of the address member is a JSON + // structure containing some or all of the members defined in http://openid.net/specs/openid-connect-basic-1_0-32.html#AddressClaim + public const string Address = "address"; + // + // Summary: + // Audience(s) that this ID Token is intended for. It MUST contain the OAuth 2.0 + // client_id of the Relying Party as an audience value. It MAY also contain identifiers + // for other audiences. In the general case, the aud value is an array of case sensitive + // strings. In the common special case when there is one audience, the aud value + // MAY be a single case sensitive string. + public const string Audience = "aud"; + // + // Summary: + // The reference token identifier + public const string ReferenceTokenId = "reference_token_id"; + // + // Summary: + // The confirmation + public const string Confirmation = "cnf"; + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ChartData.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ChartData.cs new file mode 100644 index 0000000..b23d5a0 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ChartData.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace WalkingTec.Mvvm.Core +{ + public class ChartData + { + //一级分类 + public string Category { get; set; } + + public double Value { get; set; } + //散点图 x值 + public double ValueX { get; set; } + + //二级分类 不同数据集 + public string Series { get; set; } + + public double Addition { get; set; } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ClaimComparer.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ClaimComparer.cs new file mode 100644 index 0000000..cc6b2f5 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ClaimComparer.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Security.Claims; + +namespace WalkingTec.Mvvm.Core.Auth +{ + /// + /// Compares two instances of Claim + /// + public class ClaimComparer : EqualityComparer + { + /// + /// Claim comparison options + /// + public class Options + { + /// + /// Specifies if the issuer value is being taken into account + /// + public bool IgnoreIssuer { get; set; } = false; + + /// + /// Specifies if claim and issuer value comparison should be case-sensitive + /// + public bool IgnoreValueCase { get; set; } = false; + } + + private readonly Options _options = new Options(); + + /// + /// Initializes a new instance of the class with default options. + /// + public ClaimComparer() + { } + + /// + /// Initializes a new instance of the class with given comparison options. + /// + /// Comparison options. + public ClaimComparer(Options options) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + + /// + public override bool Equals(Claim x, Claim y) + { + if (x == null && y == null) return true; + if (x == null && y != null) return false; + if (x != null && y == null) return false; + + StringComparison valueComparison = StringComparison.Ordinal; + if (_options.IgnoreValueCase == true) valueComparison = StringComparison.OrdinalIgnoreCase; + + var equal = (String.Equals(x.Type, y.Type, StringComparison.OrdinalIgnoreCase) && + String.Equals(x.Value, y.Value, valueComparison) && + String.Equals(x.ValueType, y.ValueType, StringComparison.Ordinal)); + + + if (_options.IgnoreIssuer) + { + return equal; + } + else + { + return (equal && String.Equals(x.Issuer, y.Issuer, valueComparison)); + } + } + + /// + public override int GetHashCode(Claim claim) + { + if (claim is null) return 0; + + int typeHash = claim.Type?.ToLowerInvariant().GetHashCode() ?? 0 ^ claim.ValueType?.GetHashCode() ?? 0; + int valueHash; + int issuerHash; + + if (_options.IgnoreValueCase) + { + valueHash = claim.Value?.ToLowerInvariant().GetHashCode() ?? 0; + issuerHash = claim.Issuer?.ToLowerInvariant().GetHashCode() ?? 0; + } + else + { + valueHash = claim.Value?.GetHashCode() ?? 0; + issuerHash = claim.Issuer?.GetHashCode() ?? 0; + } + + if (_options.IgnoreIssuer) + { + return typeHash ^ valueHash; + + } + else + { + return typeHash ^ valueHash ^ issuerHash; + } + } + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ColumnFormatInfo.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ColumnFormatInfo.cs new file mode 100644 index 0000000..7b68b3b --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ColumnFormatInfo.cs @@ -0,0 +1,135 @@ +using System; + +namespace WalkingTec.Mvvm.Core +{ + + public class ColumnFormatInfo + { + public ColumnFormatTypeEnum FormatType { get; set; } + + public ButtonTypesEnum ButtonType { get; set; } + public string Text { get; set; } + public string Title { get; set; } + + public string Script { get; set; } + + public string WindowID { get; set; } + + public string Url { get; set; } + + public bool ShowDialog { get; set; } + + public bool Resizable { get; set; } + + public string ButtonID { get; set; } + + public int? Width { get; set; } + + public int? Height { get; set; } + + public Guid? FileID { get; set; } + + public string Html { get; set; } + + public bool Maxed { get; set; } + + public string ButtonClass { get; set; } + + public RedirectTypesEnum RType { get; set; } + + public string Style { get; set; } + public static ColumnFormatInfo MakeDialogButton(ButtonTypesEnum buttonType, string url, string buttonText, int? width, int? height, string title = null, string buttonID = null, bool showDialog = true, bool resizable = true, bool maxed = false,string buttonclass = null, string style=null) + { + ColumnFormatInfo rv = new ColumnFormatInfo(); + rv.FormatType = ColumnFormatTypeEnum.Dialog; + rv.ButtonType = buttonType; + rv.Url = url; + rv.Width = width; + rv.Height = height; + rv.Text = buttonText; + rv.Title = title; + rv.ButtonID = buttonID; + rv.ShowDialog = showDialog; + rv.Resizable = resizable; + rv.Maxed = maxed; + rv.ButtonClass = buttonclass; + rv.Style = style; + return rv; + } + + public static ColumnFormatInfo MakeScriptButton(ButtonTypesEnum buttonType, string url, string buttonText, string buttonID = null, string script = "", string buttonclass = null, string style = null) + { + ColumnFormatInfo rv = new ColumnFormatInfo(); + rv.FormatType = ColumnFormatTypeEnum.Script; + rv.ButtonType = buttonType; + rv.Url = url; + rv.Text = buttonText; + rv.ButtonID = buttonID; + rv.Script = script; + rv.ButtonClass = buttonclass; + rv.Style = style; + return rv; + } + + public static ColumnFormatInfo MakeButton(ButtonTypesEnum buttonType, string url, string buttonText, int? width, int? height, string title = null, string buttonID = null, bool resizable = true, bool maxed = false, string buttonclass = null, string style = null, RedirectTypesEnum rtype = RedirectTypesEnum.Layer) + { + ColumnFormatInfo rv = new ColumnFormatInfo(); + rv.FormatType = ColumnFormatTypeEnum.Button; + rv.ButtonType = buttonType; + rv.Url = url; + rv.Width = width; + rv.Height = height; + rv.Text = buttonText; + rv.Title = title; + rv.ButtonID = buttonID; + rv.Resizable = resizable; + rv.Maxed = maxed; + rv.ButtonClass = buttonclass; + rv.RType = rtype; + rv.Style = style; + return rv; + } + + public static ColumnFormatInfo MakeDownloadButton(ButtonTypesEnum buttonType, Guid? fileID, string buttonText = null, string buttonclass = null, string style = null) + { + ColumnFormatInfo rv = new ColumnFormatInfo(); + rv.FormatType = ColumnFormatTypeEnum.Download; + rv.ButtonType = buttonType; + rv.FileID = fileID; + rv.Text = buttonText?? CoreProgram._localizer?["Sys.Download"]; + rv.ButtonClass = buttonclass; + rv.Style = style; + return rv; + } + + public static ColumnFormatInfo MakeViewButton(ButtonTypesEnum buttonType, Guid? fileID, int? width = null, int? height = null, string title = null, string windowID = null, string buttonText = null, bool resizable = true, bool maxed = false, string buttonclass = null, string style = null) + { + ColumnFormatInfo rv = new ColumnFormatInfo(); + rv.FormatType = ColumnFormatTypeEnum.ViewPic; + rv.ButtonType = buttonType; + rv.FileID = fileID; + rv.Width = width; + rv.Height = height; + rv.WindowID = windowID; + rv.Text = buttonText ?? CoreProgram._localizer?["Sys.Preview"]; + rv.Title = title ?? CoreProgram._localizer?["Sys.Preview"]; + rv.Resizable = resizable; + rv.Maxed = maxed; + rv.ButtonClass = buttonclass; + rv.Style = style; + return rv; + } + + public static ColumnFormatInfo MakeHtml(string html) + { + ColumnFormatInfo rv = new ColumnFormatInfo(); + rv.FormatType = ColumnFormatTypeEnum.Html; + rv.Html = html; + if (string.IsNullOrEmpty(rv.Html) == false) + { + rv.Html += ""; + } + return rv; + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/CommonEqualityComparer.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/CommonEqualityComparer.cs new file mode 100644 index 0000000..2e773f2 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/CommonEqualityComparer.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace WalkingTec.Mvvm.Core +{ + public class CommonEqualityComparer : IEqualityComparer + { + private Func keySelector; + + public CommonEqualityComparer(Func keySelector) + { + this.keySelector = keySelector; + } + + public bool Equals(T x, T y) + { + return EqualityComparer.Default.Equals(keySelector(x), keySelector(y)); + } + + public int GetHashCode(T obj) + { + return EqualityComparer.Default.GetHashCode(keySelector(obj)); + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/DataPrivilegeInfo.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/DataPrivilegeInfo.cs new file mode 100644 index 0000000..7343546 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/DataPrivilegeInfo.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using WalkingTec.Mvvm.Core.Extensions; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// 数据权限信息接口 + /// + public interface IDataPrivilege + { + //数据权限关联的表名 + string ModelName { get; set; } + //数据权限名称 + string PrivillegeName { get; set; } + + Type ModelType { get; set; } + //获取数据权限的下拉菜单 + List GetItemList (WTMContext wtmcontext, string filter = null, List ids = null); + List GetTreeParentIds(WTMContext wtmcontext, List dps); + List GetTreeSubIds(WTMContext wtmcontext, List pids); + } + + /// + /// 数据权限信息 + /// + /// 关联表类 + public class DataPrivilegeInfo : IDataPrivilege where T : TopBasePoco + { + //数据权限关联的表名 + public string ModelName { get; set; } + //数据权限名称 + public string PrivillegeName { get; set; } + //显示字段 + private Expression> _displayField; + //where过滤条件 + private Expression> _where; + public Type ModelType { get; set; } + + public DataPrivilegeInfo(string name, Expression> displayField, Expression> where = null) + { + ModelType = typeof(T); + ModelName = ModelType.Name; + PrivillegeName = name; + _displayField = displayField; + _where = where; + } + + /// + /// 获取数据权限的下拉菜单 + /// + /// wtm context + /// filter + /// ids + /// 数据权限关联表的下拉菜单 + public List GetItemList(WTMContext wtmcontext, string filter = null,List ids= null) + { + var user = wtmcontext?.LoginUserInfo; + Expression> where = null; + + if (ids != null) + { + where = ids.GetContainIdExpression(); + } + else + { + if (string.IsNullOrEmpty(filter) == false) + { + ChangePara cp = new ChangePara(); + ParameterExpression pe = Expression.Parameter(typeof(T)); + //var toString = Expression.Call(cp.Change(_displayField.Body, pe), "Cast", new Type[] { typeof(string)}); + var tolower = Expression.Call(cp.Change(_displayField.Body, pe), "ToLower", new Type[] { }); + var exp = Expression.Call(tolower, "Contains", null, Expression.Constant(filter.ToLower())); + where = Expression.Lambda>(exp, pe); + if (_where != null) + { + var temp = cp.Change(_where.Body, pe); + var together = Expression.And(where.Body, temp); + where = Expression.Lambda>(together, pe); + } + } + else + { + where = _where; + } + if (where == null) + { + where = x => 1 == 1; + } + } + List rv = new List(); + if (user.Roles?.Where(x => x.RoleCode == "001").FirstOrDefault() == null && user.DataPrivileges?.Where(x => x.RelateId == null).FirstOrDefault() == null) + { + rv = wtmcontext.DC.Set().CheckIDs(user.DataPrivileges.Select(y => y.RelateId).ToList()).Where(where).GetSelectListItems(wtmcontext, _displayField, null, ignorDataPrivilege: true); + } + else + { + rv = wtmcontext.DC.Set().Where(where).GetSelectListItems(wtmcontext, _displayField, null, ignorDataPrivilege: true); + } + return rv; + } + + public List GetTreeParentIds(WTMContext wtmcontext, List dps) + { + var ids = dps.Where(x => x.TableName == this.ModelName).Select(x => x.RelateId).ToList(); + var idscheck = ids.GetContainIdExpression(); + var modified = wtmcontext.DC.Set().Where(idscheck).CheckNotNull("ParentId").DynamicSelect("ParentId"); + var skipids = modified.ToList(); + return skipids; + } + + public List GetTreeSubIds(WTMContext wtmcontext, List pids) + { + ParameterExpression pe = Expression.Parameter(typeof(T)); + Expression parentid = Expression.Property(pe, typeof(T).GetSingleProperty("ParentId")); + return wtmcontext.DC.Set().Where(pids.GetContainIdExpression(parentid)).DynamicSelect("ID").ToList(); + } + } + + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/DateRange.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/DateRange.cs new file mode 100644 index 0000000..d85cb1d --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/DateRange.cs @@ -0,0 +1,451 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace WalkingTec.Mvvm.Core +{ + public sealed class DateRange + { + private static readonly DateTime UtCDefaultEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + private static readonly DateTime DefaultEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Local); + + private static readonly DateTimeTypeEnum DefaultType = DateTimeTypeEnum.DateTime; + + + private DateRange() { } + + + public DateRange(DateTime startTime, DateTime endTime) : this(startTime, endTime, DefaultType, DefaultEpoch) { } + + public DateRange(DateTime startTime, DateTime endTime, DateTimeTypeEnum type, DateTime epoch) + { + Type = type; + Epoch = epoch; + SetStartTime(startTime); + SetEndTime(endTime); + } + + + public DateRange(TimeSpan startSpan, TimeSpan endSpan) : this(startSpan, endSpan, DefaultType, DefaultEpoch) { } + + public DateRange(TimeSpan startSpan, TimeSpan endSpan, DateTimeTypeEnum type, DateTime epoch) + { + Type = type; + Epoch = epoch; + SetStartTime(epoch.Add(startSpan)); + SetEndTime(epoch.Add(endSpan)); + } + + public DateRange(DateTimeOffset startOffset, DateTimeOffset endOffset) : this(startOffset, endOffset, DefaultType, DefaultEpoch) { } + + public DateRange(DateTimeOffset startOffset, DateTimeOffset endOffset, DateTimeTypeEnum type, DateTime epoch) + { + Type = type; + Epoch = epoch; + SetStartTime(startOffset.DateTime); + SetEndTime(endOffset.DateTime); + } + + public readonly DateTime Epoch = DefaultEpoch; + + public readonly DateTimeTypeEnum Type = DefaultType; + + + public string Value => ToString(); + + + private DateTime? _startTime; + + public DateTime? GetStartTime() + { + return _startTime; + } + + public void SetStartTime(DateTime? value) + { + if (value == null) return; + + value = SwitchTime(value.Value); + + if (_endTime.HasValue && _endTime.Value < value) return; + _startTime = value; + } + + private DateTime? _endTime; + + public DateTime? GetEndTime() + { + switch (Type) + { + case DateTimeTypeEnum.Date: + return _endTime?.AddDays(1); + case DateTimeTypeEnum.DateTime when _startTime.HasValue && _endTime.HasValue && _startTime.Value == _endTime.Value: + case DateTimeTypeEnum.DateTime when _endTime.HasValue && _endTime.Value.Hour == 0 && _endTime.Value.Minute == 0 && _endTime.Value.Second == 0 && _endTime.Value.Millisecond == 0: + return _endTime?.AddDays(1); + default: + return _endTime; + } + } + + public void SetEndTime(DateTime? value) + { + if (value == null) return; + + value = SwitchTime(value.Value); + + if (_startTime.HasValue && _startTime.Value > value) return; + _endTime = value; + } + + + public DateTimeOffset? GetStartOffset() + { + if (_startTime == null) + return null; + return _startTime <= DateTimeOffset.MinValue.LocalDateTime ? DateTimeOffset.MinValue : new DateTimeOffset(GetStartTime().Value); + } + + public void SetStartOffset(DateTimeOffset? value) + { + if (value == null) return; + SetStartTime(value.Value.LocalDateTime); + } + + public DateTimeOffset? GetEndOffset() + { + if (_endTime == null) + return null; + return _endTime <= DateTimeOffset.MinValue.LocalDateTime ? DateTimeOffset.MinValue : new DateTimeOffset(GetEndTime().Value); + } + + public void SetEndOffset(DateTimeOffset? value) + { + if (value == null) return; + SetEndTime(value.Value.LocalDateTime); + } + + + public TimeSpan? GetStartSpan() + { + if (_endTime == null) + return null; + return GetStartTime() - Epoch.ToLocalTime(); + } + + public void SetStartSpan(TimeSpan? value) + { + if (value == null) return; + SetStartTime(Epoch.Add(value.Value)); + } + + public TimeSpan? GetEndSpan() + { + if (_endTime == null) + return null; + return GetEndTime() - Epoch.ToLocalTime(); + } + + public void SetEndSpan(TimeSpan? value) + { + if (value == null) return; + SetEndTime(Epoch.Add(value.Value)); + } + + public override string ToString() + { + return ToString(DateTimeFormatDic[Type], "~"); + } + + public string ToString(string format) + { + return ToString(format, "~"); + } + + public string ToString(string format, string rangeSplit) + { + if (_startTime.HasValue && _endTime.HasValue) + return $"{_startTime?.ToString(format)} {rangeSplit} {_endTime?.ToString(format)}"; + return string.Empty; + } + + public static readonly Dictionary DateTimeFormatDic = new Dictionary() + { + { DateTimeTypeEnum.Date,"yyyy-MM-dd"}, + { DateTimeTypeEnum.DateTime,"yyyy-MM-dd HH:mm:ss"}, + { DateTimeTypeEnum.Year,"yyyy"}, + { DateTimeTypeEnum.Month,"yyyy-MM"}, + { DateTimeTypeEnum.Time,"HH:mm:ss"}, + }; + + private DateTime SwitchTime(DateTime time) + { + if (Epoch.Kind == time.Kind) + return time; + switch (Epoch.Kind) + { + case DateTimeKind.Local: + switch (time.Kind) + { + case DateTimeKind.Unspecified: + return new DateTime(time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second, time.Millisecond, DateTimeKind.Local); + case DateTimeKind.Utc: + return time.ToLocalTime(); + } + break; + case DateTimeKind.Unspecified: + switch (time.Kind) + { + case DateTimeKind.Local: + return new DateTime(time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second, time.Millisecond, DateTimeKind.Unspecified); + case DateTimeKind.Utc: + return new DateTime(time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second, time.Millisecond, DateTimeKind.Unspecified); + } + break; + case DateTimeKind.Utc: + switch (time.Kind) + { + case DateTimeKind.Local: + return time.ToUniversalTime(); + case DateTimeKind.Unspecified: + return new DateTime(time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second, time.Millisecond, DateTimeKind.Utc); + } + break; + } + return time; + } + + public static DateRange Default => Today; + + public static DateRange NinetyDays + { + get + { + var result = new DateRange(DateTime.Today.AddDays(-90), DateTime.Today); + return result; + } + } + + public static DateRange ThirtyDays + { + get + { + var result = new DateRange(DateTime.Today.AddDays(-30), DateTime.Today); + return result; + } + } + + public static DateRange TwoWeek + { + get + { + var result = new DateRange(DateTime.Today.AddDays(-14), DateTime.Today); + return result; + } + } + + public static DateRange Week + { + get + { + var result = new DateRange(DateTime.Today.AddDays(-7), DateTime.Today); + return result; + } + } + + public static DateRange Today + { + get + { + var result = new DateRange(DateTime.Today, DateTime.Today); + return result; + } + } + + + public static DateRange Yesterday + { + get + { + var result = new DateRange(DateTime.Today.AddDays(-1), DateTime.Today.AddDays(-1)); + return result; + } + } + + + public static DateRange UtcDefault => UtcToday; + + public static DateRange UtcNinetyDays + { + get + { + var result = new DateRange(DateTime.UtcNow.Date.AddDays(-90), DateTime.UtcNow.Date); + return result; + } + } + + public static DateRange UtcThirtyDays + { + get + { + var result = new DateRange(DateTime.UtcNow.Date.AddDays(-30), DateTime.UtcNow.Date, DefaultType, UtCDefaultEpoch); + return result; + } + } + + public static DateRange UtcTwoWeek + { + get + { + var result = new DateRange(DateTime.UtcNow.Date.AddDays(-14), DateTime.UtcNow.Date, DefaultType, UtCDefaultEpoch); + return result; + } + } + + public static DateRange UtcWeek + { + get + { + var result = new DateRange(DateTime.UtcNow.Date.AddDays(-7), DateTime.UtcNow.Date, DefaultType, UtCDefaultEpoch); + return result; + } + + } + + public static DateRange UtcToday + { + get + { + var result = new DateRange(DateTime.UtcNow.Date.AddDays(-1), DateTime.UtcNow.Date, DefaultType, UtCDefaultEpoch); + return result; + } + } + + public static DateRange UtcYesterday + { + get + { + var result = new DateRange(DateTime.UtcNow.Date.AddDays(-1), DateTime.UtcNow.Date, DefaultType, UtCDefaultEpoch); + return result; + } + } + + + private static readonly Dictionary DateTimeRegexDic = new Dictionary() + { + { DateTimeTypeEnum.DateTime,@"((\d{4}|\d{3}|\d{2}|\d{1})[-](1[0-2]|0?[1-9])[-](3[01]|[12][0-9]|0?[1-9])\s+(20|21|22|23|[0-1]\d):[0-5]\d:[0-5]\d)"}, + { DateTimeTypeEnum.Date,@"(\d{4}|\d{3}|\d{2}|\d{1})[-](1[0-2]|0?[1-9])[-](3[01]|[12][0-9]|0?[1-9])"}, + { DateTimeTypeEnum.Month,@"(\d{4}|\d{3}|\d{2}|\d{1})[-](1[0-2]|0?[1-9])"}, + { DateTimeTypeEnum.Time,@"(20|21|22|23|[0-1]\d):[0-5]\d:[0-5]\d"}, + { DateTimeTypeEnum.Year,@"(\d{4}|\d{3}|\d{2}|\d{1})"}, + }; + + public static bool TryParse(string input, out DateRange result) + { + if (TryParse(input, new[] { '~' }, out result)) return true; + result = null; + foreach (var pair in DateTimeRegexDic) + { + if (Regex.IsMatch(input, pair.Value)) + { + var values = Regex.Matches(input, pair.Value, RegexOptions.IgnorePatternWhitespace); + return values.Count == 2 && TryParse(values[0].Value, values[1].Value, DefaultEpoch, out result); + } + } + return false; + } + + public static bool TryParse(string input, char[] separator, out DateRange result) + { + result = null; + if (string.IsNullOrEmpty(input)) + { + return false; + } + var values = input.Split(separator, StringSplitOptions.RemoveEmptyEntries); + return values.Length == 2 && TryParse(values[0], values[1], DefaultEpoch, out result); + } + + public static bool TryParse(string input, string[] separator, out DateRange result) + { + result = null; + if (string.IsNullOrEmpty(input)) + { + return false; + } + var values = input.Split(separator, StringSplitOptions.RemoveEmptyEntries); + return values.Length == 2 && TryParse(values[0], values[1], DefaultEpoch, out result); + } + + public static bool TryParse(string[] input, out DateRange result) + { + result = null; + return input.Length == 2 && TryParse(input[0], input[1], DefaultEpoch, out result); + } + + + public static bool TryParse(string startTime, string endTime, DateTime epoch, out DateRange result) + { + result = null; + switch (startTime.Trim().Length) + { + //Year + case 4: + { + if (!int.TryParse(startTime, out var y1)) return false; + if (y1 < 1 || y1 > 9999) return false; + + if (!int.TryParse(endTime, out var y2)) return false; + if (y2 < 1 || y2 > 9999) return false; + + result = new DateRange(new DateTime(y1, 1, 1, 0, 0, 0, epoch.Kind), new DateTime(y2, 1, 1, 0, 0, 0, epoch.Kind)); + return true; + + } + //Month + case 7: + { + if (!DateTime.TryParse(startTime, out var v1)) return false; + + if (!DateTime.TryParse(endTime, out var v2)) return false; + + result = new DateRange(v1, v2, DateTimeTypeEnum.Month, epoch); + + return true; + } + //Time + case 8: + { + if (!DateTime.TryParse(startTime, out var v1)) return false; + + if (!DateTime.TryParse(endTime, out var v2)) return false; + + result = new DateRange(v1, v2, DateTimeTypeEnum.Time, epoch); + + return true; + } + //Date + case 10: + { + if (!DateTime.TryParse(startTime, out var v1)) return false; + + if (!DateTime.TryParse(endTime, out var v2)) return false; + + result = new DateRange(v1, v2, DateTimeTypeEnum.Date, epoch); + + return true; + } + //DateTime + default: + { + if (!DateTime.TryParse(startTime, out var v1)) return false; + + if (!DateTime.TryParse(endTime, out var v2)) return false; + + result = new DateRange(v1, v2, DateTimeTypeEnum.DateTime, epoch); + + return true; + } + } + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/DuplicateInfo.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/DuplicateInfo.cs new file mode 100644 index 0000000..d444675 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/DuplicateInfo.cs @@ -0,0 +1,341 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using WalkingTec.Mvvm.Core.Extensions; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// 重复数据组 + /// + /// 重复数据类 + public class DuplicatedGroup + { + public List> Fields { get; set; } + } + + /// + /// 重复数据信息 + /// + /// 数据类 + public class DuplicatedInfo + { + //重复数据分组 + public List> Groups { get; set; } + + public DuplicatedInfo() + { + Groups = new List>(); + } + + /// + /// 添加一组重复信息,一组中的多个字段必须同时重复才认为是重复数据 + /// + /// 一个或多个重复数据字段 + public DuplicatedInfo AddGroup(params DuplicatedField[] FieldExps) + { + DuplicatedGroup newGroup = new DuplicatedGroup() + { + Fields = new List>() + }; + foreach (var exp in FieldExps) + { + newGroup.Fields.Add(exp); + } + Groups.Add(newGroup); + return this; + } + } + + /// + /// 简单重复数据字段信息 + /// + /// 重复数据类 + public class DuplicatedField + { + //直接可顺序关联出的字段 + protected Expression> _directFieldExp { get; set; } + + public virtual List GetProperties() + { + List rv = new List + { + PropertyHelper.GetPropertyInfo(_directFieldExp) + }; + return rv; + } + + + /// + /// 根据设定的字段,生成查询重复数据的Lambda,最终返回类似 x=>x.property == val的lambda + /// + /// 要验证字段的实体类 + /// ParameterExpression + /// + public virtual Expression GetExpression(T Entity, ParameterExpression para) + { + var propName = PropertyHelper.GetPropertyName(_directFieldExp); + var prop = PropertyHelper.GetPropertyInfo(_directFieldExp); + var func = _directFieldExp.Compile(); + var val = func.Invoke(Entity); + + ////如果字段值为null则跳过,因为一般情况下null值不会被认为重复 + //if (val == null) + //{ + // return res; + //} + + //如果字段值是空字符串,则跳过 + if (val is string && val.ToString() == string.Empty) + { + var requiredAttrs = prop.GetCustomAttributes(typeof(RequiredAttribute), false).ToList(); + + if (requiredAttrs == null || requiredAttrs.Count == 0) + { + return null; + } + else + { + var requiredAtt = requiredAttrs[0] as RequiredAttribute; + if (requiredAtt.AllowEmptyStrings == true) + { + return null; + } + } + } + //生成一个表达式,类似于 x=>x.field == val + var splits = propName.Split('.'); + var idproperty = typeof(T).GetSingleProperty(splits[0]); + + Expression left = Expression.Property(para, idproperty); + for (int i = 1; i < splits.Length; i++) + { + var tempproperty = typeof(T).GetSingleProperty(splits[i]); + left = Expression.Property(left, tempproperty); + } + + if (val != null && left.Type.IsGeneric(typeof(Nullable<>))) + { + left = Expression.Property(left, "Value"); + } + if (left.Type == typeof(string)) + { + left = Expression.Call(left, typeof(String).GetMethod("Trim", Type.EmptyTypes)); + } + if (val is string) + { + val = val.ToString().Trim(); + } + var right = Expression.Constant(val); + var equal = Expression.Equal(left, right); + return equal; + } + + protected DuplicatedField() + { + + } + + /// + /// 创建一个包含可顺序关联出字段的简单重复字段信息 + /// + /// 字段 + /// 字段信息 + public DuplicatedField(Expression> FieldExp) + { + _directFieldExp = FieldExp; + } + + } + + /// + /// 复杂重复字段信息接口 + /// + public interface IComplexDuplicatedField + { + Type GetMiddleTableType(); + } + + + /// + /// 复杂重复数据字段信息 + /// + /// 重复数据类 + /// 重复数据关联的List中的类 + public class ComplexDuplicatedField : DuplicatedField, IComplexDuplicatedField + { + /// + /// 中间字段 + /// + private Expression>> _middleExp { get; set; } + /// + /// 最终字段 + /// + private List>> _subFieldExps { get; set; } + + protected ComplexDuplicatedField() + { + + } + + /// + /// 创建一个复杂字段 + /// + /// 中间字段类 + /// 最终字段类 + /// + public ComplexDuplicatedField(Expression>> MiddleExp, params Expression>[] FieldExps) + { + _middleExp = MiddleExp; + _subFieldExps = new List>>(); + _subFieldExps.AddRange(FieldExps); + } + + public Type GetMiddleTableType() + { + return typeof(V); + } + + /// + /// 生成验证复杂字段是否重复的Lambda + /// + /// 源数据 + /// 源数据类型 + /// Where语句 + public override Expression GetExpression(T Entity, ParameterExpression para) + { + ParameterExpression midPara = Expression.Parameter(typeof(V), "tm2"); + //获取中间表的List + var list = _middleExp.Compile().Invoke(Entity); + if (list == null) + { + return null; + } + List allExp = new List(); + Expression rv = null; + //循环中间表数据 + foreach (var li in list) + { + List innerExp = new List(); + bool needBreak = false; + //循环中间表要检查重复的字段 + foreach (var SubFieldExp in _subFieldExps) + { + //拼接字段表达式,使left等于类似 x.field 的形式 + Expression left = Expression.Property(midPara, SubFieldExp.GetPropertyName()); + //如果字段是nullable类型的,则拼接value,形成类似 x.field.Value的形式 + if (left.Type.IsGeneric(typeof(Nullable<>)) == true) + { + left = Expression.Property(left, "Value"); + } + //如果字段是string类型,则拼接trim,形成类似 x.field.Trim()的形式 + if (left.Type == typeof(string)) + { + left = Expression.Call(left, typeof(String).GetMethod("Trim", Type.EmptyTypes)); + } + //使用当前循环的中间表的数据获取字段的值 + object vv = SubFieldExp.Compile().Invoke(li); + //如果值为空则跳过 + if (vv == null) + { + needBreak = true; + continue; + } + //如果值为空字符串且没要求必填,则跳过 + if (vv is string && vv.ToString() == "") + { + var requiredAttrs = li.GetType().GetSingleProperty(SubFieldExp.GetPropertyName()).GetCustomAttributes(typeof(RequiredAttribute), false).ToList(); + + if (requiredAttrs == null || requiredAttrs.Count == 0) + { + needBreak = true; + continue; + } + else + { + var requiredAtt = requiredAttrs[0] as RequiredAttribute; + if (requiredAtt.AllowEmptyStrings == true) + { + needBreak = true; + continue; + } + } + } + //如果值为字符串,调用trim函数 + if (vv is string) + { + vv = vv.ToString().Trim(); + } + //拼接形成 x.field == value的形式 + ConstantExpression right = Expression.Constant(vv); + BinaryExpression equal = Expression.Equal(left, right); + innerExp.Add(equal); + } + if (needBreak) + { + continue; + } + //拼接多个 x.field==value,形成 x.field==value && x.field1==value1 .....的形式 + Expression exp = null; + if (innerExp.Count == 1) + { + exp = innerExp[0]; + } + if (innerExp.Count > 1) + { + exp = Expression.And(innerExp[0], innerExp[1]); + for (int i = 2; i < innerExp.Count; i++) + { + exp = Expression.And(exp, innerExp[i]); + } + } + //调用any函数,形成 .Any(x=> x.field==value && x.field1==value1....)的形式 + if (exp != null) + { + var any = Expression.Call( + typeof(Enumerable), + "Any", + new Type[] { typeof(V) }, + Expression.Property(para, _middleExp.GetPropertyName()), + Expression.Lambda>(exp, new ParameterExpression[] { midPara })); + allExp.Add(any); + } + } + //拼接多个any函数形成 .Any(x=> x.field==value && x.field1==value1....) || .Any(x=> x.field==value && x.field1==value1....)的形式并返回 + if (allExp.Count == 1) + { + rv = allExp[0]; + } + if (allExp.Count > 1) + { + rv = Expression.OrElse(allExp[0], allExp[1]); + for (int i = 2; i < allExp.Count; i++) + { + rv = Expression.OrElse(rv, allExp[i]); + } + } + return rv; + } + + /// + /// 获取字段属性 + /// + /// 字段属性列表 + public override List GetProperties() + { + List rv = new List(); + foreach (var subField in _subFieldExps) + { + var pro = subField.GetPropertyInfo(); + if (pro != null) + { + rv.Add(pro); + } + } + return rv; + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/DynamicData.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/DynamicData.cs new file mode 100644 index 0000000..e4178ba --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/DynamicData.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WalkingTec.Mvvm.Core +{ + public class DynamicData : DynamicObject + { + public Dictionary Fields = new Dictionary(); + + public int Count { get { return Fields.Keys.Count; } } + + public void Add(string name, object val = null) + { + if (!Fields.ContainsKey(name)) + { + Fields.Add(name, val); + } + else + { + Fields[name] = val; + } + } + + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + if (Fields.ContainsKey(binder.Name)) + { + result = Fields[binder.Name]; + return true; + } + return base.TryGetMember(binder, out result); + } + + public override bool TrySetMember(SetMemberBinder binder, object value) + { + if (!Fields.ContainsKey(binder.Name)) + { + Fields.Add(binder.Name, value); + } + else + { + Fields[binder.Name] = value; + } + return true; + } + + public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) + { + if (Fields.ContainsKey(binder.Name) && + Fields[binder.Name] is Delegate) + { + Delegate del = Fields[binder.Name] as Delegate; + result = del.DynamicInvoke(args); + return true; + } + return base.TryInvokeMember(binder, args, out result); + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ExcelPropety.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ExcelPropety.cs new file mode 100644 index 0000000..8a714bc --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ExcelPropety.cs @@ -0,0 +1,497 @@ +using NPOI.HSSF.UserModel; +using NPOI.OpenXmlFormats.Spreadsheet; +using NPOI.SS.UserModel; +using NPOI.SS.Util; +using NPOI.XSSF.UserModel; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Linq.Expressions; +using WalkingTec.Mvvm.Core.Extensions; + +namespace WalkingTec.Mvvm.Core +{ + public class ExcelPropety + { + #region 属性 + private string _columnName; + /// + /// 列名 + /// + public string ColumnName + { + get + { + string col = _columnName; + return col; + } + set + { + _columnName = value; + } + } + + public string FieldDisplayName { get; set; } + + public string FieldName { get; set; } + + //private string _backgroudColor; + ///// + ///// 背景色 + ///// + //public string BackgroudColor + //{ + // get { return _backgroudColor; } + // set { _backgroudColor = value; } + //} + + /// + /// 背景色 + /// + public BackgroudColorEnum BackgroudColor { get; set; } + + private Type _resourceType; + /// + /// 多语言 + /// + public Type ResourceType + { + get { return _resourceType; } + set { _resourceType = value; } + } + + private ColumnDataType _dataType; + /// + /// 数据类型 + /// + public ColumnDataType DataType + { + get { return _dataType; } + set { _dataType = value; } + } + + private Type _enumType; + public Type EnumType + { + get { return _enumType; } + set + { + _enumType = value; + this.ListItems = _enumType.ToListItems(); + } + } + + private string _minValueOrLength; + /// + /// 最小长度 + /// + public string MinValueOrLength + { + get { return _minValueOrLength; } + set { _minValueOrLength = value; } + } + + private string _maxValuseOrLength; + /// + /// 最大长度 + /// + public string MaxValuseOrLength + { + get { return _maxValuseOrLength; } + set { _maxValuseOrLength = value; } + } + + private bool _isNullAble; + /// + /// 是否可以为空 + /// + public bool IsNullAble + { + get { return _isNullAble; } + set { _isNullAble = value; } + } + + private IEnumerable _listItems; + /// + /// 类表中数据 + /// + public IEnumerable ListItems + { + get { return _listItems; } + set { _listItems = value; } + } + + private object _value; + /// + /// Value + /// + public object Value + { + get { return _value; } + set { _value = value; } + } + + private List _dynamicColumns; + /// + /// 动态列 + /// + public List DynamicColumns + { + get { return _dynamicColumns == null ? new List() : _dynamicColumns; } + set { _dynamicColumns = value; } + } + + public Type SubTableType { get; set; } + + public bool ReadOnly { get; set; } + /// + /// 字符数量 + /// + public int CharCount { get; set; } + + #endregion + + #region 设定Excel数据验证 + /// + /// 设置Excel单元格样式(标题),数据格式 + /// + /// 数据类型 + /// 单元格索引 + /// Sheet页 + /// 数据Sheet页 + /// 样式 + /// 格式 + public void SetColumnFormat(ColumnDataType dateType, int porpetyIndex, ISheet sheet, ISheet dataSheet, ICellStyle dataStyle, IDataFormat dataFormat) + { + XSSFDataValidationHelper dvHelper = new XSSFDataValidationHelper((XSSFSheet)sheet); + CellRangeAddressList CellRangeList = new CellRangeAddressList(1, 1048576 - 1, porpetyIndex, porpetyIndex); //超过1048576最大行数,打开Excel会报错 + XSSFDataValidationConstraint dvConstraint = null; + XSSFDataValidation dataValidation = null; + + switch (dateType) + { + case ColumnDataType.Date: + case ColumnDataType.DateTime: + //因为DateTime类型,添加Validation报错,所以去掉 + dataStyle.DataFormat = dataFormat.GetFormat("yyyy-MM-dd HH:mm:ss"); + break; + case ColumnDataType.Number: + this.MinValueOrLength = string.IsNullOrEmpty(this.MinValueOrLength) ? long.MinValue.ToString() : this.MinValueOrLength; + this.MaxValuseOrLength = string.IsNullOrEmpty(this.MaxValuseOrLength) ? long.MaxValue.ToString() : this.MaxValuseOrLength; + dvConstraint = (XSSFDataValidationConstraint)dvHelper.CreateNumericConstraint(ValidationType.INTEGER, OperatorType.BETWEEN, this.MinValueOrLength, this.MaxValuseOrLength); + dataValidation = (XSSFDataValidation)dvHelper.CreateValidation(dvConstraint, CellRangeList); + dataValidation.CreateErrorBox(CoreProgram._localizer?["Sys.Error"], CoreProgram._localizer?["Sys.PleaseInputNumber"]); + dataStyle.DataFormat = dataFormat.GetFormat("0"); + dataValidation.CreatePromptBox(CoreProgram._localizer?["Sys.PleaseInputNumberFormat"], CoreProgram._localizer?["Sys.DataRange", MinValueOrLength, MaxValuseOrLength]); + break; + case ColumnDataType.Float: + this.MinValueOrLength = string.IsNullOrEmpty(this.MinValueOrLength) ? decimal.MinValue.ToString() : this.MinValueOrLength; + this.MaxValuseOrLength = string.IsNullOrEmpty(this.MaxValuseOrLength) ? decimal.MaxValue.ToString() : this.MaxValuseOrLength; + dvConstraint = (XSSFDataValidationConstraint)dvHelper.CreateNumericConstraint(ValidationType.DECIMAL, OperatorType.BETWEEN, this.MinValueOrLength, this.MaxValuseOrLength); + dataValidation = (XSSFDataValidation)dvHelper.CreateValidation(dvConstraint, CellRangeList); + dataValidation.CreateErrorBox(CoreProgram._localizer?["Sys.Error"], CoreProgram._localizer?["Sys.PleaseInputDecimal"]); + dataStyle.DataFormat = HSSFDataFormat.GetBuiltinFormat("0.00"); + dataValidation.CreatePromptBox(CoreProgram._localizer?["Sys.PleaseInputDecimalFormat"], CoreProgram._localizer?["Sys.DataRange", MinValueOrLength, MaxValuseOrLength]); + break; + case ColumnDataType.Bool: + dvConstraint = (XSSFDataValidationConstraint)dvHelper.CreateFormulaListConstraint("Sheet1!$A$1:$B$1"); + dataValidation = (XSSFDataValidation)dvHelper.CreateValidation(dvConstraint, CellRangeList); + dataValidation.CreateErrorBox(CoreProgram._localizer?["Sys.Error"], CoreProgram._localizer?["Sys.PleaseInputExistData"]); + dataValidation.CreatePromptBox(CoreProgram._localizer?["Sys.ComboBox"], CoreProgram._localizer?["Sys.PleaseInputExistData"]); + break; + case ColumnDataType.Text: + this.MinValueOrLength = string.IsNullOrEmpty(this.MinValueOrLength) ? "0" : this.MinValueOrLength; + this.MaxValuseOrLength = string.IsNullOrEmpty(this.MaxValuseOrLength) ? "2000" : this.MaxValuseOrLength; + dvConstraint = (XSSFDataValidationConstraint)dvHelper.CreateNumericConstraint(ValidationType.TEXT_LENGTH, OperatorType.BETWEEN, this.MinValueOrLength, this.MaxValuseOrLength); + dataValidation = (XSSFDataValidation)dvHelper.CreateValidation(dvConstraint, CellRangeList); + dataValidation.CreateErrorBox(CoreProgram._localizer?["Sys.Error"], CoreProgram._localizer?["Sys.WrongTextLength"]); + dataStyle.DataFormat = dataFormat.GetFormat("@"); + dataValidation.CreatePromptBox(CoreProgram._localizer?["Sys.PleaseInputText"], CoreProgram._localizer?["Sys.DataRange", MinValueOrLength, MaxValuseOrLength]); + break; + case ColumnDataType.ComboBox: + case ColumnDataType.Enum: + int count = this.ListItems.Count() == 0 ? 1 : this.ListItems.Count(); + string cloIndex = ""; + if (porpetyIndex > 25) + { + cloIndex += Convert.ToChar((int)(Math.Floor(porpetyIndex / 26d)) - 1 + 65); + } + cloIndex += Convert.ToChar(65 + porpetyIndex % 26).ToString(); + IName range = sheet.Workbook.CreateName(); + range.RefersToFormula = "Sheet2!$" + cloIndex + "$1:$" + cloIndex + "$" + count; + range.NameName = "dicRange" + porpetyIndex; + dvConstraint = (XSSFDataValidationConstraint)dvHelper.CreateFormulaListConstraint("dicRange" + porpetyIndex); + dataValidation = (XSSFDataValidation)dvHelper.CreateValidation(dvConstraint, CellRangeList); + dataValidation.CreateErrorBox(CoreProgram._localizer?["Sys.Error"], CoreProgram._localizer?["Sys.PleaseInputExistData"]); + var listItemsTemp = this.ListItems.ToList(); + for (int rowIndex = 0; rowIndex < this.ListItems.Count(); rowIndex++) + { + IRow dataSheetRow = dataSheet.GetRow(rowIndex); + if (dataSheetRow == null) + { + dataSheetRow = dataSheet.CreateRow(rowIndex); + } + dataSheetRow.CreateCell(porpetyIndex).SetCellValue(listItemsTemp[rowIndex].Text); + dataStyle.DataFormat = dataFormat.GetFormat("@"); + dataSheetRow.Cells.Where(x => x.ColumnIndex == porpetyIndex).FirstOrDefault().CellStyle = dataStyle; + } + dataValidation.CreatePromptBox(CoreProgram._localizer?["Sys.ComboBox"], CoreProgram._localizer?["Sys.PleaseInputExistData"]); + break; + default: + dvConstraint = (XSSFDataValidationConstraint)dvHelper.CreateNumericConstraint(ValidationType.TEXT_LENGTH, OperatorType.BETWEEN, this.MinValueOrLength, this.MaxValuseOrLength); + dataValidation = (XSSFDataValidation)dvHelper.CreateValidation(dvConstraint, CellRangeList); + dataValidation.CreateErrorBox(CoreProgram._localizer?["Sys.Error"], CoreProgram._localizer?["Sys.WrongTextLength"]); + dataStyle.DataFormat = HSSFDataFormat.GetBuiltinFormat("@"); + break; + } + if (dataValidation == null) + { + return; + } + if (!this.IsNullAble) + { + dataValidation.EmptyCellAllowed = false; + } + sheet.SetDefaultColumnStyle(porpetyIndex, dataStyle); + dataValidation.ShowErrorBox = true; + sheet.AddValidationData(dataValidation); + } + #endregion + + #region 验证Excel数据 + /// + /// 验证Value 并生成错误信息(edit by dufei 2014-06-12,修改了当列设置为不验证时候,下拉列表获取不到值的问题) + /// + /// + /// + /// + public void ValueValidity(string value, List errorMessage, int rowIndex) + { + if (this.IsNullAble && string.IsNullOrEmpty(value)) + { + this.Value = value; + } + else + { + ErrorMessage err = null; + switch (this.DataType) + { + case ColumnDataType.Date: + case ColumnDataType.DateTime: + DateTime tryDateTimeResult; + if (!DateTime.TryParse(value, out tryDateTimeResult)) + { + err = new ErrorMessage { Index = rowIndex, Message = CoreProgram._localizer?["Sys.{0}formaterror", this.ColumnName] }; + } + this.Value = tryDateTimeResult; + break; + case ColumnDataType.Number: + int tryIntResult; + if (!int.TryParse(value, out tryIntResult)) + { + err = new ErrorMessage { Index = rowIndex, Message = CoreProgram._localizer?["Sys.{0}formaterror", this.ColumnName] }; + } + this.Value = tryIntResult; + break; + case ColumnDataType.Float: + decimal tryDecimalResult; + if (!decimal.TryParse(value, out tryDecimalResult)) + { + err = new ErrorMessage { Index = rowIndex, Message = CoreProgram._localizer?["Sys.{0}formaterror", this.ColumnName] }; + } + this.Value = tryDecimalResult; + break; + case ColumnDataType.Bool: + if (value == CoreProgram._localizer?["Sys.Yes"]) + { + this.Value = true; + } + else if (value == CoreProgram._localizer?["Sys.No"]) + { + this.Value = false; + } + else + { + err = new ErrorMessage { Index = rowIndex, Message = CoreProgram._localizer?["Sys.{0}formaterror", this.ColumnName] }; + } + break; + case ColumnDataType.Text: + this.Value = value; + break; + case ColumnDataType.ComboBox: + case ColumnDataType.Enum: + if (!this.ListItems.Any(x => x.Text == value)) + { + err = new ErrorMessage { Index = rowIndex, Message = CoreProgram._localizer?["Sys.{0}ValueNotExist", this.ColumnName] }; + } + else + { + this.Value = this.ListItems.Where(x => x.Text == value).FirstOrDefault().Value; + } + break; + default: + err = new ErrorMessage { Index = rowIndex, Message = CoreProgram._localizer?["Sys.{0}ValueTypeNotAllowed", this.ColumnName] }; + break; + } + + if (err != null && this.SubTableType == null) + { + errorMessage.Add(err); + } + } + } + #endregion + + #region 自定义委托处理excel数据 + /// + /// 处理为多列数据 + /// + public CopyData FormatData; + /// + /// 处理为单列数据 + /// + public CopySingleData FormatSingleData; + + #endregion + + public static ExcelPropety CreateProperty(Expression> field, bool isDateTime = false) + { + ExcelPropety cp = new ExcelPropety(); + cp.FieldDisplayName = field.GetPropertyDisplayName(); + var fname = field.GetPropertyName(); + Type t = field.GetPropertyInfo().PropertyType; + if (fname.Contains('.')) + { + int index = fname.LastIndexOf('.'); + cp.FieldName = fname.Substring(index + 1); + cp.SubTableType = field.GetPropertyInfo().DeclaringType; + } + else + { + cp.FieldName = fname; + } + if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + var req = field.GetPropertyInfo().GetCustomAttributes(typeof(RequiredAttribute), false).Cast().FirstOrDefault(); + if (req == null) + { + cp.IsNullAble = true; + } + t = t.GenericTypeArguments[0]; + } + if (t == typeof(int) || t == typeof(long) || t == typeof(short)) + { + var sl = t.GetCustomAttributes(typeof(RangeAttribute), false).Cast().FirstOrDefault(); + cp.DataType = ColumnDataType.Number; + if (sl != null) + { + if (sl.Maximum != null) + { + cp.MaxValuseOrLength = sl.Maximum.ToString(); + } + if (sl.Minimum != null) + { + cp.MinValueOrLength = sl.Minimum.ToString(); + } + } + } + else if (t == typeof(float) || t == typeof(double) || t == typeof(decimal)) + { + cp.DataType = ColumnDataType.Float; + } + else if (t == typeof(bool)) + { + cp.DataType = ColumnDataType.Bool; + } + else if (t.IsEnum) + { + cp.DataType = ColumnDataType.Enum; + cp.EnumType = t; + } + else if (t == typeof(DateTime)) + { + cp.DataType = ColumnDataType.Date; + if (isDateTime) + cp.DataType = ColumnDataType.DateTime; + } + else + { + var sl = field.GetPropertyInfo().GetCustomAttributes(typeof(StringLengthAttribute), false).Cast().FirstOrDefault(); + var req = field.GetPropertyInfo().GetCustomAttributes(typeof(RequiredAttribute), false).Cast().FirstOrDefault(); + cp.DataType = ColumnDataType.Text; + if (req == null) + { + cp.IsNullAble = true; + } + if (sl != null) + { + if (sl.MaximumLength != 0) + { + cp.MaxValuseOrLength = sl.MaximumLength + ""; + } + if (sl.MinimumLength != 0) + { + cp.MinValueOrLength = sl.MinimumLength + ""; + } + } + } + cp.CharCount = 20; + return cp; + } + + } + + #region 辅助类型 + + /// + /// 定义处理excel为单个字段的委托 + /// + /// excel中的值 + /// excel中的值 + /// 实体的值 + /// 错误消息,没有错误为空 + public delegate void CopySingleData(object excelValue, BaseTemplateVM excelTemplate, out string entityValue, out string errorMsg); + /// + /// 定义处理excel为多个字段的委托 + /// + /// excel中的值 + /// excel中的值 + /// 返回的处理结果 + public delegate ProcessResult CopyData(object excelValue, BaseTemplateVM excelTemplate); + + /// + /// 处理结果 + /// + public class ProcessResult + { + public List EntityValues { get; set; } + public ProcessResult() + { + EntityValues = new List(); + } + } + + /// + /// 单字段类 + /// + public class EntityValue + { + /// + /// 字段名称 + /// + public string FieldName { get; set; } + /// + /// 字段值 + /// + public string FieldValue { get; set; } + /// + /// 错误消息 + /// + public string ErrorMsg { get; set; } + } + + public enum ColumnDataType { Text, Number, Date, Float, Bool, ComboBox, Enum, Dynamic, DateTime } + + #endregion +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ExpressionVisitors.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ExpressionVisitors.cs new file mode 100644 index 0000000..cffe9a0 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ExpressionVisitors.cs @@ -0,0 +1,676 @@ +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using WalkingTec.Mvvm.Core.Extensions; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// 分析x==y这种类型的表达式,并储存在Dictionary中 + /// + public class SetValuesParser : ExpressionVisitor + { + private Dictionary _rv = new Dictionary(); + + /// + /// 开始分析表达式 + /// + /// 源表达式 + /// 将x==y这种表达式变为Dictionary并返回 + public Dictionary Parse(Expression expression) + { + Visit(expression); + return _rv; + } + + /// + /// 处理所有二进制类型的表达式 + /// + /// 当前表达式节点 + /// 修改后的表达式 + protected override Expression VisitBinary(BinaryExpression node) + { + //如果表达式是x==y的类型,则获取y的值,并使用x最为key,y的值作为value添加到Dictionary中保存 + if (node.NodeType == ExpressionType.Equal) + { + var pi = PropertyHelper.GetPropertyName(node.Left.NodeType == ExpressionType.Convert ? ((UnaryExpression)node.Left).Operand : node.Left); + if (!_rv.ContainsKey(pi)) + { + _rv.Add(pi, Expression.Lambda(node.Right).Compile().DynamicInvoke()); + } + } + return base.VisitBinary(node); + } + } + + /// + /// 替换表达式中的OrderBy语句 + /// + public class OrderReplaceModifier : ExpressionVisitor + { + private bool _addMode = false; + private SortInfo _sortinfo; + + /// + /// 构造函数 + /// + /// sortinfo + public OrderReplaceModifier(SortInfo sortinfo) + { + _sortinfo = sortinfo; + } + + /// + /// 修改where + /// + /// 表达式 + /// 修改后的表达式 + public Expression Modify(Expression expression) + { + //先调用一次Visit,删除所有的where表达式 + var rv = Visit(expression); + if ((rv is QueryRootExpression) || rv.Type.IsGeneric(typeof(EnumerableQuery<>))) + { + var modelType = rv.Type.GenericTypeArguments[0]; + ParameterExpression pe = Expression.Parameter(modelType, "x"); + Expression left1 = Expression.Constant(1); + Expression right1 = Expression.Constant(1); + Expression trueExp = Expression.Equal(left1, right1); + rv = Expression.Call( +typeof(Queryable), +"Where", +new Type[] { modelType }, +rv, +Expression.Lambda(trueExp, new ParameterExpression[] { pe })); + + } + //将模式设为addMode,再调用一次Visit来添加新的表达式 + _addMode = true; + rv = Visit(rv); + return rv; + } + + /// + /// 向表达式树上层寻找不是where的节点 + /// + /// 表达式 + /// 返回表达式上层第一个不是where的节点 + private Expression GetParentExpNotOrder(MethodCallExpression exp) + { + var parentNode = exp.Arguments[0] as MethodCallExpression; + if (parentNode == null || (parentNode.Method.Name.ToLower() != "orderby" && parentNode.Method.Name.ToLower() != "orderbydescending")) + { + if (parentNode == null) + { + return exp.Arguments[0]; + } + return parentNode; + } + else + { + return GetParentExpNotOrder(parentNode); + } + } + + /// + /// 检查方法调用类型的表达式 + /// + /// 表达式节点 + /// 修改后的表达式 + protected override Expression VisitMethodCall(MethodCallExpression node) + { + //如果不是添加模式,那么删除所有的order + if (_addMode == false) + { + var aType = node.Arguments[0].Type; + //如果节点是order + if (node != null && (node.Method.Name.ToLower() == "orderby" || node.Method.Name.ToLower() == "orderbydescending" || node.Method.Name.ToLower() == "thenby" || node.Method.Name.ToLower() == "thenbydescending") && aType.GetTypeInfo().IsGenericType) + { + //继续往上找到不是where的节点 + return GetParentExpNotOrder(node); + } + } + //如果是添加模式 + else + { + var modelType = node.Type.GenericTypeArguments[0]; + List info = new List() { _sortinfo }; + Expression rv = null; + foreach (var item in info) + { + var idproperty = modelType.GetSingleProperty(item.Property); + if (idproperty == null) + { + return node; + } + var reftype = idproperty.DeclaringType; + ParameterExpression pe = Expression.Parameter(modelType, "x"); + Expression pro = Expression.Property(pe, reftype.GetSingleProperty(item.Property)); + Type proType = idproperty.PropertyType; + if (item.Direction == SortDir.Asc) + { + if (rv == null) + { + rv = Expression.Call( + typeof(Queryable), + "OrderBy", + new Type[] { modelType, proType }, + node, + Expression.Lambda(pro, new ParameterExpression[] { pe })); + } + else + { + rv = Expression.Call( + typeof(Queryable), + "ThenBy", + new Type[] { modelType, proType }, + rv, + Expression.Lambda(pro, new ParameterExpression[] { pe })); + } + } + if (item.Direction == SortDir.Desc) + { + if (rv == null) + { + rv = Expression.Call( + typeof(Queryable), + "OrderByDescending", + new Type[] { modelType, proType }, + node, + Expression.Lambda(pro, new ParameterExpression[] { pe })); + } + else + { + rv = Expression.Call( + typeof(Queryable), + "ThenByDescending", + new Type[] { modelType, proType }, + rv, + Expression.Lambda(pro, new ParameterExpression[] { pe })); + } + } + } + return rv; + + } + return base.VisitMethodCall(node); + } + } + + + /// + /// 替换表达式中的Where语句 + /// + public class WhereReplaceModifier : ExpressionVisitor where T : TopBasePoco + { + private Type _modelType; + private bool _addMode = false; + private Expression> _where; + + /// + /// 构造函数 + /// + /// 需要替换的新where语句 + public WhereReplaceModifier(Expression> where) + { + _where = where; + } + + /// + /// 获取表达式的源数据类 + /// + /// 表达式 + /// 表达式的源数据类型 + private Type GetDCModel(Expression expression) + { + //如果表达式是方法调用的类型,则一直向上寻找,知道找到参数为ObjectQuery<>的表达式,它的类型就是整个表达式的源数据类型 + if (expression.NodeType == ExpressionType.Call) + { + var exp = (expression as MethodCallExpression).Arguments[0]; + if (exp is QueryRootExpression) + { + return exp.Type.GenericTypeArguments[0]; + } + else + { + return GetDCModel(exp); + } + } + else if (expression.NodeType == ExpressionType.Constant) + { + if (expression is QueryRootExpression || expression.Type.IsGeneric(typeof(EnumerableQuery<>))) + { + return expression.Type.GenericTypeArguments[0]; + } + else + { + return null; + } + } + else + { + return null; + } + } + + /// + /// 修改where + /// + /// 表达式 + /// 修改后的表达式 + public Expression Modify(Expression expression) + { + //获取源类型 + var checkType = GetDCModel(expression); + + if (checkType != null) + { + _modelType = checkType; + //先调用一次Visit,删除所有的where表达式 + var rv = Visit(expression); + //将模式设为addMode,再调用一次Visit来添加新的表达式 + _addMode = true; + rv = Visit(rv); + return rv; + } + else + { + return expression; + } + } + + /// + /// 向表达式树上层寻找不是where的节点 + /// + /// 表达式 + /// 返回表达式上层第一个不是where的节点 + private Expression GetParentExpNotWhere(MethodCallExpression exp) + { + if (!(exp.Arguments[0] is MethodCallExpression parentNode)) + { + return exp.Arguments[0]; + } + else if (parentNode.Method.Name.ToLower() != "where") + { + return parentNode; + } + else + { + return GetParentExpNotWhere(parentNode); + } + } + + protected override Expression VisitExtension(Expression node) + { + if (_addMode == true && node is QueryRootExpression) + { + if (node.Type.GenericTypeArguments[0] == _modelType) + { + ParameterExpression pe = Expression.Parameter(_modelType); + ChangePara cp = new ChangePara(); + //传递过来的where条件,其源类型不一定和表达式中原有的相符,所以先将新where条件的源类型修改成和query一样的 + //也就是说新的where条件可能是 x=>x.id==1, 而在表达式中不一定有x,所以要先统一 + var modifiedWhere = cp.Change(_where.Body, pe); + var rv = Expression.Call( + typeof(Queryable), + "Where", + new Type[] { _modelType }, + node, + Expression.Lambda(modifiedWhere, new ParameterExpression[] { pe })); + return rv; + } + } + return base.VisitExtension(node); + } + + /// + /// 检查方法调用类型的表达式 + /// + /// 表达式节点 + /// 修改后的表达式 + protected override Expression VisitMethodCall(MethodCallExpression node) + { + //如果不是添加模式,那么删除所有的where条件 + if (_addMode == false) + { + if (node.Arguments.Count == 0) + { + return base.VisitMethodCall(node); + } + var aType = node.Arguments[0].Type; + //如果节点的上一个节点是where + if (node.Arguments[0] is MethodCallExpression parentNode && parentNode.Method.Name.ToLower() == "where" && aType.GetTypeInfo().IsGenericType) + { + //继续往上找到不是where的节点 + var nowhereNode = GetParentExpNotWhere(parentNode); + Type gType = aType.GetGenericTypeDefinition(); + Type argType = aType.GenericTypeArguments[0]; + //使用上面不是where的节点直接拼接本节点,从而删除了中间的where + if ((gType == typeof(IQueryable<>) || gType == typeof(QueryRootExpression)) && argType == _modelType) + { + var paras = new List + { + nowhereNode + }; + paras.AddRange(node.Arguments.Skip(1).ToList()); + var rv = Expression.Call( + node.Method, + paras); + return rv; + } + } + } + return base.VisitMethodCall(node); + } + } + + /// + /// 用于PersistPoco的搜索,检查查询语句中是否有IsValid的搜索条件,如果没有,默认加上IsValid=true + /// + public class IsValidModifier : ExpressionVisitor + { + private Type _modelType; + private bool _needAdd = true; + private int _mode = 0; //0是搜素模式,1是添加模式 + private Expression> _where; + + /// + /// 构造函数 + /// + public IsValidModifier() + { + _where = x => x.IsValid == true; + } + + /// + /// 获取表达式的源数据类 + /// + /// 表达式 + /// 表达式的源数据类型 + private Type GetDCModel(Expression expression) + { + //如果表达式是方法调用的类型,则一直向上寻找,知道找到参数为ObjectQuery<>的表达式,它的类型就是整个表达式的源数据类型 + if (expression.NodeType == ExpressionType.Call) + { + var exp = (expression as MethodCallExpression).Arguments[0]; + if (exp is QueryRootExpression) + { + return exp.Type.GenericTypeArguments[0]; + } + else + { + return GetDCModel(exp); + } + } + else if (expression.NodeType == ExpressionType.Constant) + { + if (expression is QueryRootExpression || expression.Type.IsGeneric(typeof(EnumerableQuery<>))) + { + return expression.Type.GenericTypeArguments[0]; + } + else + { + return null; + } + } + else + { + return null; + } + } + + /// + /// 修改where + /// + /// 表达式 + /// 修改后的表达式 + public Expression Modify(Expression expression) + { + //获取源类型 + var checkType = GetDCModel(expression); + + if (checkType != null && typeof(IPersistPoco).IsAssignableFrom( checkType)) + { + _modelType = checkType; + //先调用一次Visit,删除所有的where表达式 + var rv = Visit(expression); + //将模式设为addMode,再调用一次Visit来添加新的表达式 + if (_needAdd == true) + { + _mode = 1; + rv = Visit(rv); + } + return rv; + } + else + { + return expression; + } + } + + /// + /// 向表达式树上层寻找不是where的节点 + /// + /// 表达式 + /// 返回表达式上层第一个不是where的节点 + private Expression GetParentExpNotWhere(MethodCallExpression exp) + { + if (!(exp.Arguments[0] is MethodCallExpression parentNode)) + { + return exp.Arguments[0]; + } + else if (parentNode.Method.Name.ToLower() != "where") + { + return parentNode; + } + else + { + return GetParentExpNotWhere(parentNode); + } + } + + protected override Expression VisitExtension(Expression node) + { + if (_mode == 1 && node is QueryRootExpression) + { + if (node.Type.GenericTypeArguments[0] == _modelType) + { + ParameterExpression pe = Expression.Parameter(_modelType); + ChangePara cp = new ChangePara(); + //传递过来的where条件,其源类型不一定和表达式中原有的相符,所以先将新where条件的源类型修改成和query一样的 + //也就是说新的where条件可能是 x=>x.id==1, 而在表达式中不一定有x,所以要先统一 + var modifiedWhere = cp.Change(_where.Body, pe); + var rv = Expression.Call( + typeof(Queryable), + "Where", + new Type[] { _modelType }, + node, + Expression.Lambda(modifiedWhere, new ParameterExpression[] { pe })); + return rv; + } + } + return base.VisitExtension(node); + } + + /// + /// 查找where中是否出现过对于IsValid的判断 + /// + /// + /// + protected override Expression VisitBinary(BinaryExpression node) + { + //如果表达式是x==y的类型,则获取y的值,并使用x最为key,y的值作为value添加到Dictionary中保存 + if (_mode == 0) + { + if (node.NodeType == ExpressionType.Equal) + { + var pi = PropertyHelper.GetPropertyName(node.Left); + if (pi.ToLower() == "isvalid") + { + _needAdd = false; + } + } + } + return base.VisitBinary(node); + } + + } + + + /// + /// 修改表达式参数 + /// + public class ChangePara : ExpressionVisitor + { + ParameterExpression _pe; + + /// + /// 修改参数 + /// + /// 要修改的表达式 + /// 新的参数 + /// 修改后的表达式 + public Expression Change(Expression expression, ParameterExpression pe) + { + _pe = pe; + return Visit(expression); + } + + /// + /// 检查所有参数类型的表达式 + /// + /// 表达式节点 + /// 新的参数类型 + protected override Expression VisitParameter(ParameterExpression node) + { + return _pe; + } + + /// + /// 检查所有成员访问类型的表达式 + /// + /// 表达式节点 + /// 修改后的表达式 + protected override Expression VisitMember(MemberExpression node) + { + if (node.Expression.NodeType == ExpressionType.Parameter) + { + var rv = Expression.MakeMemberAccess(_pe, node.Member); + return rv; + } + else + { + return base.VisitMember(node); + } + } + } + + /// + /// 获取Select语句中选择的列的信息 + /// + public class SelectInfo : ExpressionVisitor + { + private List _columns; + private bool _found; + /// + /// 构造函数 + /// + public SelectInfo() + { + } + + /// + /// 获取Select语句中选择的列的名字 + /// + /// 表达式 + /// 列的名字 + public List GetColumns(Expression expression) + { + Visit(expression); + return _columns; + } + + + /// + /// 检查方法调用类型的表达式 + /// + /// 表达式节点 + /// 修改后的表达式 + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (_found == false) + { + if (node.Method.Name.ToLower() == "select") + { + if (node.Arguments[1] is UnaryExpression ue) + { + var inner = ue.Operand as LambdaExpression; + if (inner.Body is MemberInitExpression memberinit) + { + _columns = new List(); + foreach (var m in memberinit.Bindings) + { + _columns.Add(m.Member.Name); + } + _found = true; + } + } + } + } + return base.VisitMethodCall(node); + } + } + + public class ClearSelectMany : ExpressionVisitor + { + /// + /// 构造函数 + /// + public ClearSelectMany() + { + } + + /// + /// 获取Select语句中选择的列的名字 + /// + /// 表达式 + /// 列的名字 + public Expression Clear(Expression expression) + { + return Visit(expression); + } + + protected override Expression VisitMemberInit(MemberInitExpression node) + { + List newbinding = new List(); + for (int i = 0; i < node.Bindings.Count; i++) + { + bool islist = false; + if (node.Bindings[i] is MemberAssignment ma && ma.Expression.Type.GetTypeInfo().IsGenericType == true) + { + if (ma.Expression.Type.GetGenericTypeDefinition() != typeof(Nullable<>)) + { + islist = true; + } + } + if (islist == false) + { + newbinding.Add(node.Bindings[i]); + } + } + if (newbinding.Count < node.Bindings.Count) + { + return node.Update(node.NewExpression, newbinding); + } + else + { + return base.VisitMemberInit(node); + } + } + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ExtraClass.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ExtraClass.cs new file mode 100644 index 0000000..8f1ffd2 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ExtraClass.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// 简单属性结构类 + /// + public class SimpleTreeTextAndValue + { + + public object Id { get; set; } + public object Text { get; set; } + public object ParentId { get; set; } + } + + /// + /// 简单键值对类,用来存著生成下拉菜单的数据 + /// + public class SimpleTextAndValue + { + public object Text { get; set; } + public object Value { get; set; } + } + + /// + /// 数据库排序类 + /// + public class SortInfo + { + public string Property { get; set; } + public SortDir Direction { get; set; } + } + + public class ApiResult + { + public T Data { get; set; } + public HttpStatusCode? StatusCode { get; set; } + public ErrorObj Errors { get; set; } + public string ErrorMsg { get; set; } + + public ApiResult() + { + Data = default(T); + } + } + + public class ErrorObj + { + public Dictionary Form { get; set; } + public List Message { get; set; } + + public string GetFirstError() + { + if(Message != null && Message.Any()) + { + return Message.First(); + } + if(Form != null && Form.Any()) + { + return Form.First().Value; + } + return ""; + } + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/FileHandlers/IWtmFileHandler.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/FileHandlers/IWtmFileHandler.cs new file mode 100644 index 0000000..ec2a145 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/FileHandlers/IWtmFileHandler.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using WalkingTec.Mvvm.Core.Models; + +namespace WalkingTec.Mvvm.Core.Support.FileHandlers +{ + public interface IWtmFileHandler + { + (string path,string handlerInfo) Upload(string fileName, long fileLength, Stream data, string group=null, string subdir=null, string extra=null); + Stream GetFileData(IWtmFile file); + + void DeleteFile(IWtmFile file); + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmDataBaseFileHandler.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmDataBaseFileHandler.cs new file mode 100644 index 0000000..45a7bf3 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmDataBaseFileHandler.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Linq; +using System.Text; +using WalkingTec.Mvvm.Core.Extensions; +using WalkingTec.Mvvm.Core.Models; + +namespace WalkingTec.Mvvm.Core.Support.FileHandlers +{ + [Display(Name = "database")] + public class WtmDataBaseFileHandler : WtmFileHandlerBase + { + private static string _modeName = "database"; + + public WtmDataBaseFileHandler(Configs config, IDataContext dc) : base(config, dc) + { + } + + public override Stream GetFileData(IWtmFile file) + { + var rv = _dc.Set().CheckID(file.GetID()).FirstOrDefault(); + if (rv != null) + { + return new MemoryStream((rv as FileAttachment).FileData); + } + return null; + } + + + public IWtmFile UploadToDB(string fileName, long fileLength, Stream data, string groupName = null, string subdir = null, string extra = null) + { + FileAttachment file = new FileAttachment(); + file.FileName = fileName; + file.Length = fileLength; + file.UploadTime = DateTime.Now; + file.SaveMode = _modeName; + file.ExtraInfo = extra; + var ext = string.Empty; + if (string.IsNullOrEmpty(fileName) == false) + { + var dotPos = fileName.LastIndexOf('.'); + ext = fileName.Substring(dotPos + 1); + } + file.FileExt = ext; + using (var dataStream = new MemoryStream()) + { + data.CopyTo(dataStream); + file.FileData = dataStream.ToArray(); + } + _dc.AddEntity(file); + _dc.SaveChanges(); + return file; + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmFileHandlerBase.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmFileHandlerBase.cs new file mode 100644 index 0000000..363bab8 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmFileHandlerBase.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.EntityFrameworkCore; +using WalkingTec.Mvvm.Core.Extensions; +using WalkingTec.Mvvm.Core.Models; + +namespace WalkingTec.Mvvm.Core.Support.FileHandlers +{ + public abstract class WtmFileHandlerBase : IWtmFileHandler + { + protected Configs _config; + protected IDataContext _dc; + + public WtmFileHandlerBase(Configs config, IDataContext dc) + { + _config = config; + _dc = dc; + if (_dc == null) + { + _dc = _config.CreateDC(); + } + } + + + public virtual void DeleteFile(IWtmFile file) + { + } + + public virtual Stream GetFileData(IWtmFile file) + { + return null; + } + + public virtual (string path, string handlerInfo) Upload(string fileName, long fileLength, Stream data, string group=null, string subdir = null, string extra=null) + { + return ("", ""); + } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmFileProvider.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmFileProvider.cs new file mode 100644 index 0000000..0e67060 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmFileProvider.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using Microsoft.Extensions.Options; +using WalkingTec.Mvvm.Core.Extensions; +using WalkingTec.Mvvm.Core.Models; + +namespace WalkingTec.Mvvm.Core.Support.FileHandlers +{ + public class WtmFileProvider + { + public string SaveMode { get; set; } + private static Dictionary _handlers; + private static ConstructorInfo _defaultHandler; + private WTMContext _wtm; + public static Func _subDirFunc; + + public WtmFileProvider(WTMContext wtm) + { + _wtm = wtm; + } + + public static void Init(Configs config, GlobalData gd) + { + _handlers = new Dictionary(); + var types = gd.GetTypesAssignableFrom(); + int count = 1; + foreach (var item in types) + { + var cons = item.GetConstructor(new Type[] { typeof(Configs), typeof(IDataContext) }); + var nameattr = item.GetCustomAttribute(); + string name = ""; + if (nameattr == null) + { + name = "FileHandler" + count; + count++; + } + else + { + name = nameattr.Name; + } + name = name.ToLower(); + if (name == config.FileUploadOptions.SaveFileMode.ToString().ToLower()) + { + _defaultHandler = cons; + } + _handlers.Add(name, cons); + } + if (_defaultHandler == null && types.Count > 0) + { + _defaultHandler = types[0].GetConstructor(new Type[] { typeof(Configs), typeof(IDataContext) }); + } + + } + + public IWtmFileHandler CreateFileHandler(string saveMode = null, IDataContext dc = null) + { + ConstructorInfo ci = null; + if(dc == null) + { + dc = _wtm.CreateDC(); + } + if (string.IsNullOrEmpty(saveMode)) + { + ci = _defaultHandler; + } + else + { + saveMode = saveMode.ToLower(); + if (_handlers.ContainsKey(saveMode)) + { + ci = _handlers[saveMode]; + } + } + if (ci == null) + { + return new WtmDataBaseFileHandler(_wtm.ConfigInfo, dc); + } + else + { + return ci.Invoke(new object[] { _wtm.ConfigInfo, dc }) as IWtmFileHandler; + } + } + + public IWtmFile Upload(string fileName, long fileLength, Stream data, string group = null, string subdir = null, string extra = null, string saveMode = null, IDataContext dc =null) + { + if (dc == null) + { + dc = _wtm.CreateDC(); + } + var fh = CreateFileHandler(saveMode, dc); + if (fh is WtmDataBaseFileHandler lfh) + { + return lfh.UploadToDB(fileName, fileLength, data, group, subdir, extra); + } + else + { + var rv = fh.Upload(fileName, fileLength, data, group, subdir, extra); + if (string.IsNullOrEmpty(rv.path) == false) + { + FileAttachment file = new FileAttachment(); + file.FileName = fileName; + file.Length = fileLength; + file.UploadTime = DateTime.Now; + file.SaveMode = string.IsNullOrEmpty(saveMode) == true ? _wtm.ConfigInfo.FileUploadOptions.SaveFileMode : saveMode; + file.ExtraInfo = extra; + var ext = string.Empty; + if (string.IsNullOrEmpty(fileName) == false) + { + var dotPos = fileName.LastIndexOf('.'); + ext = fileName[(dotPos + 1)..]; + } + file.FileExt = ext; + file.Path = rv.path; + file.HandlerInfo = rv.handlerInfo; + dc.AddEntity(file); + dc.SaveChanges(); + return file; + } + else + { + return null; + } + } + } + + public IWtmFile GetFile(string id, bool withData = true, IDataContext dc = null) + { + IWtmFile rv; + if (dc == null) + { + dc = _wtm.CreateDC(); + } + rv = dc.Set().CheckID(id).Select(x => new FileAttachment + { + ID = x.ID, + ExtraInfo = x.ExtraInfo, + FileExt = x.FileExt, + FileName = x.FileName, + Length = x.Length, + Path = x.Path, + SaveMode = x.SaveMode, + UploadTime = x.UploadTime + }).FirstOrDefault(); + if (rv != null && withData == true) + { + var fh = CreateFileHandler(rv.SaveMode, dc); + rv.DataStream = fh.GetFileData(rv); + } + return rv; + + } + + public void DeleteFile(string id, IDataContext dc = null) + { + FileAttachment file = null; + if (dc == null) + { + dc = _wtm.CreateDC(); + } + file = dc.Set().CheckID(id) + .Select(x => new FileAttachment + { + ID = x.ID, + ExtraInfo = x.ExtraInfo, + FileExt = x.FileExt, + FileName = x.FileName, + Path = x.Path, + SaveMode = x.SaveMode, + Length = x.Length, + UploadTime = x.UploadTime + }) + .FirstOrDefault(); + if (file != null) + { + try + { + dc.Set().Remove(file); + dc.SaveChanges(); + var fh = CreateFileHandler(file.SaveMode, dc); + fh.DeleteFile(file); + } + catch { } + } + + } + + + public string GetFileName(string id, IDataContext dc = null) + { + string rv; + if (dc == null) + { + dc = _wtm.CreateDC(); + } + rv = dc.Set().CheckID(id).Select(x => x.FileName).FirstOrDefault(); + return rv; + } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmLocalFileHandler.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmLocalFileHandler.cs new file mode 100644 index 0000000..a370451 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmLocalFileHandler.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Linq; +using System.Text; +using WalkingTec.Mvvm.Core.Extensions; +using WalkingTec.Mvvm.Core.Models; + +namespace WalkingTec.Mvvm.Core.Support.FileHandlers +{ + + [Display(Name = "local")] + public class WtmLocalFileHandler : WtmFileHandlerBase + { + + public WtmLocalFileHandler(Configs config, IDataContext dc) : base(config, dc) + { + } + + public override Stream GetFileData(IWtmFile file) + { + return File.OpenRead(GetFullPath(file.Path)); + } + + + public override (string path, string handlerInfo) Upload(string fileName, long fileLength, Stream data, string group = null, string subdir = null, string extra = null) + { + var localSettings = _config.FileUploadOptions.Settings.Where(x => x.Key.ToLower() == "local").Select(x => x.Value).FirstOrDefault(); + + var groupdir = ""; + if (string.IsNullOrEmpty(group)) + { + groupdir = localSettings?.FirstOrDefault().GroupLocation; + } + else { + groupdir = localSettings?.Where(x => x.GroupName.ToLower() == group.ToLower()).FirstOrDefault().GroupLocation; + } + if (string.IsNullOrEmpty(groupdir)) + { + groupdir = "./uploads"; + } + string pathHeader = groupdir; + if (string.IsNullOrEmpty(subdir) == false) + { + pathHeader = Path.Combine(pathHeader, subdir); + } + else + { + var sub = WtmFileProvider._subDirFunc?.Invoke(this); + if(string.IsNullOrEmpty(sub)== false) + { + pathHeader = Path.Combine(pathHeader, sub); + } + } + string fulldir = GetFullPath(pathHeader); + if (!Directory.Exists(fulldir)) + { + Directory.CreateDirectory(fulldir); + } + var ext = string.Empty; + if (string.IsNullOrEmpty(fileName) == false) + { + var dotPos = fileName.LastIndexOf('.'); + ext = fileName.Substring(dotPos + 1); + } + var filename = $"{Guid.NewGuid().ToNoSplitString()}.{ext}"; + var fullPath = Path.Combine(fulldir, filename); + using (var fileStream = File.Create(fullPath)) + { + data.CopyTo(fileStream); + } + data.Dispose(); + return (Path.Combine(pathHeader, filename),""); + } + + public override void DeleteFile(IWtmFile file) + { + if (string.IsNullOrEmpty(file?.Path) == false) + { + try + { + File.Delete(GetFullPath(file?.Path)); + } + catch { } + } + } + + private string GetFullPath(string path) + { + string rv = ""; + if (path.StartsWith(".")) + { + rv = Path.Combine(_config.HostRoot, path); + } + else + { + rv = path; + } + rv = Path.GetFullPath(rv); + return rv ; + } + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmOssFileHandler.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmOssFileHandler.cs new file mode 100644 index 0000000..9db8ff4 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmOssFileHandler.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Linq; +using System.Text; +using Aliyun.OSS; +using WalkingTec.Mvvm.Core.ConfigOptions; +using WalkingTec.Mvvm.Core.Extensions; +using WalkingTec.Mvvm.Core.Models; + +namespace WalkingTec.Mvvm.Core.Support.FileHandlers +{ + + [Display(Name = "oss")] + public class WtmOssFileHandler : WtmFileHandlerBase + { + //private static string _modeName = "oss"; + + public WtmOssFileHandler(Configs config, IDataContext dc) : base(config, dc) + { + } + + public override Stream GetFileData(IWtmFile file) + { + var ossSettings = _config.FileUploadOptions.Settings.Where(x => x.Key.ToLower() == "oss").Select(x => x.Value).FirstOrDefault(); + FileHandlerOptions groupInfo = null; + if (string.IsNullOrEmpty(file.HandlerInfo)) + { + groupInfo = ossSettings?.FirstOrDefault(); + } + else + { + groupInfo = ossSettings?.Where(x => x.GroupName.ToLower() == file.HandlerInfo.ToLower()).FirstOrDefault(); + if (groupInfo == null) + { + groupInfo = ossSettings?.FirstOrDefault(); + } + } + if (groupInfo == null) + { + return null; + } + + OssClient client = new OssClient(groupInfo.ServerUrl, groupInfo.Key, groupInfo.Secret); + var rv = new MemoryStream(); + client.GetObject(new GetObjectRequest(groupInfo.GroupLocation, file.Path), rv); + rv.Position = 0; + return rv; + + } + + + public override (string path, string handlerInfo) Upload(string fileName, long fileLength, Stream data, string group = null, string subdir = null, string extra = null) + { + var ext = string.Empty; + if (string.IsNullOrEmpty(fileName) == false) + { + var dotPos = fileName.LastIndexOf('.'); + ext = fileName.Substring(dotPos + 1); + } + + var ossSettings = _config.FileUploadOptions.Settings.Where(x => x.Key.ToLower() == "oss").Select(x => x.Value).FirstOrDefault(); + FileHandlerOptions groupInfo = null; + if (string.IsNullOrEmpty(group)) + { + groupInfo = ossSettings?.FirstOrDefault(); + } + else + { + groupInfo = ossSettings?.Where(x => x.GroupName.ToLower() == group.ToLower()).FirstOrDefault(); + if (groupInfo == null) + { + groupInfo = ossSettings?.FirstOrDefault(); + } + } + if (groupInfo == null) + { + return (null,null); + } + + + string pathHeader = ""; + if (string.IsNullOrEmpty(subdir) == false) + { + pathHeader = Path.Combine(pathHeader, subdir); + } + else + { + var sub = WtmFileProvider._subDirFunc?.Invoke(this); + if (string.IsNullOrEmpty(sub) == false) + { + pathHeader = Path.Combine(pathHeader, sub); + } + } + var fullPath = Path.Combine(pathHeader, $"{Guid.NewGuid().ToNoSplitString()}.{ext}"); + fullPath = fullPath.Replace("\\", "/"); + OssClient client = new OssClient(groupInfo.ServerUrl, groupInfo.Key, groupInfo.Secret); + var result = client.PutObject(groupInfo.GroupLocation, fullPath, data); + if (result.HttpStatusCode == System.Net.HttpStatusCode.OK) + { + return (fullPath, groupInfo.GroupName); + } + else + { + return (null, null); + } + } + + public override void DeleteFile(IWtmFile file) + { + var ossSettings = _config.FileUploadOptions.Settings.Where(x => x.Key.ToLower() == "oss").Select(x => x.Value).FirstOrDefault(); + FileHandlerOptions groupInfo = null; + if (string.IsNullOrEmpty(file.HandlerInfo)) + { + groupInfo = ossSettings?.FirstOrDefault(); + } + else + { + groupInfo = ossSettings?.Where(x => x.GroupName.ToLower() == file.HandlerInfo.ToLower()).FirstOrDefault(); + if (groupInfo == null) + { + groupInfo = ossSettings?.FirstOrDefault(); + } + } + if (groupInfo == null) + { + return; + } + try + { + OssClient client = new OssClient(groupInfo.ServerUrl, groupInfo.Key, groupInfo.Secret); + client.DeleteObject(groupInfo.GroupLocation, file.Path); + } + catch { } + return; + } + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleAction.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleAction.cs new file mode 100644 index 0000000..50d969f --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleAction.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace WalkingTec.Mvvm.Core.Support.Json +{ + public class SimpleAction + { + public Guid ID { get; set; } + public ActionDescriptionAttribute ActionDes { get; set; } + public string _name; + public string ActionName + { + get + { + if (ActionDes?._localizer != null && string.IsNullOrEmpty(ActionDes?.Description) == false) + { + return ActionDes._localizer[ActionDes.Description]; + } + else + { + return _name ?? ""; + } + } + set + { + _name = value; + } + } + + public string MethodName { get; set; } + + public Guid? ModuleId { get; set; } + + public SimpleModule Module { get; set; } + + public string Parameter { get; set; } + + public List ParasToRunTest { get; set; } + + public bool IgnorePrivillege { get; set; } + + private string _url; + public string Url + { + get + { + if (_url == null) + { + if (this.Module.Area != null) + { + _url = "/" + this.Module.Area.Prefix + "/" + this.Module.ClassName + "/" + this.MethodName; + } + else + { + _url = "/" + this.Module.ClassName + "/" + this.MethodName; + } + } + return _url; + } + set + { + _url = value; + } + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleArea.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleArea.cs new file mode 100644 index 0000000..98c17d2 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleArea.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace WalkingTec.Mvvm.Core.Support.Json +{ + public class SimpleArea + { + public Guid ID { get; set; } + public string AreaName { get; set; } + public string Prefix { get; set; } + + public List Modules { get; set; } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleDataPri.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleDataPri.cs new file mode 100644 index 0000000..c87ccb8 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleDataPri.cs @@ -0,0 +1,19 @@ +using System; + +namespace WalkingTec.Mvvm.Core.Support.Json +{ + [Serializable] + public class SimpleDataPri + { + public Guid ID { get; set; } + + public string UserCode { get; set; } + public string GroupCode { get; set; } + + public string TableName { get; set; } + + public string RelateId { get; set; } + + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleFunctionPri.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleFunctionPri.cs new file mode 100644 index 0000000..47f3710 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleFunctionPri.cs @@ -0,0 +1,18 @@ +using System; + +namespace WalkingTec.Mvvm.Core.Support.Json +{ + [Serializable] + public class SimpleFunctionPri + { + public Guid ID { get; set; } + public string RoleCode{ get; set; } + + public Guid MenuItemId { get; set; } + + public string Url { get; set; } + + public bool? Allowed { get; set; } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleGroup.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleGroup.cs new file mode 100644 index 0000000..4d8ef35 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleGroup.cs @@ -0,0 +1,13 @@ +using System; + +namespace WalkingTec.Mvvm.Core.Support.Json +{ + [Serializable] + public class SimpleGroup + { + public Guid ID { get; set; } + public string GroupCode { get; set; } + public string GroupName { get; set; } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleLog.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleLog.cs new file mode 100644 index 0000000..af8687a --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleLog.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace WalkingTec.Mvvm.Core.Support.Json +{ + + public class SimpleLog + { + + public ActionLog GetActionLog() + { + return new ActionLog + { + ActionName = this.ActionName, + ActionTime = this.ActionTime, + ActionUrl = this.ActionUrl, + CreateBy = this.CreateBy, + CreateTime = this.CreateTime, + Duration = this.Duration, + IP = this.IP, + ITCode = this.ITCode, + LogType = this.LogType, + ModuleName = this.ModuleName, + Remark = this.Remark, + UpdateBy = this.UpdateBy, + UpdateTime = this.UpdateTime, + }; + } + + public string ModuleName { get; set; } + + public string ActionName { get; set; } + + public string ITCode { get; set; } + + public string ActionUrl { get; set; } + + public DateTime ActionTime { get; set; } + + public double Duration { get; set; } + + public string Remark { get; set; } + + public string IP { get; set; } + + public ActionLogTypesEnum LogType { get; set; } + + public DateTime? CreateTime { get; set; } + /// + /// CreateBy + /// + public string CreateBy { get; set; } + /// + /// UpdateTime + /// + public DateTime? UpdateTime { get; set; } + /// + /// UpdateBy + /// + public string UpdateBy { get; set; } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleMenu.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleMenu.cs new file mode 100644 index 0000000..f831a3f --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleMenu.cs @@ -0,0 +1,41 @@ +using System; + +namespace WalkingTec.Mvvm.Core.Support.Json +{ + [Serializable] + public class SimpleMenu + { + public Guid ID { get; set; } + public bool? IsInherit { get; set; } + + public Guid? ActionId { get; set; } + + public bool? IsPublic { get; set; } + + public string Url { get; set; } + + public Guid? ParentId { get; set; } + + public string PageName { get; set; } + + public int? DisplayOrder { get; set; } + + public string Icon { get; set; } + + public bool ShowOnMenu { get; set; } + } + + public class SimpleMenuApi + { + public string Id { get; set; } + + public string ParentId { get; set; } + + public string Text { get; set; } + + public string Url { get; set; } + + public string Icon { get; set; } + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleModule.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleModule.cs new file mode 100644 index 0000000..39b5216 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleModule.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace WalkingTec.Mvvm.Core.Support.Json +{ + public class SimpleModule + { + public Guid ID { get; set; } + + public ActionDescriptionAttribute ActionDes { get; set; } + + public string _name; + public string ModuleName + { + get + { + if (ActionDes?._localizer != null && string.IsNullOrEmpty(ActionDes?.Description) == false) + { + return ActionDes._localizer[ActionDes.Description]; + } + else + { + return _name ?? ""; + } + } + set + { + _name = value; + } + } + + public string ClassName { get; set; } + + public List Actions { get; set; } + + public Guid? AreaId { get; set; } + public SimpleArea Area { get; set; } + + public string NameSpace { get; set; } + + public bool IgnorePrivillege { get; set; } + + public bool IsApi { get; set; } + + public string FullName + { + get + { + return this.NameSpace + "," + this.ClassName; + } + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleRole.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleRole.cs new file mode 100644 index 0000000..35c74ec --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleRole.cs @@ -0,0 +1,13 @@ +using System; + +namespace WalkingTec.Mvvm.Core.Support.Json +{ + [Serializable] + public class SimpleRole + { + public Guid ID { get; set; } + public string RoleCode { get; set; } + public string RoleName { get; set; } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleUserInfo.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleUserInfo.cs new file mode 100644 index 0000000..98691d3 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/Json/SimpleUserInfo.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace WalkingTec.Mvvm.Core.Support.Json +{ + public class SimpleUserInfo + { + public Guid Id { get; set; } + /// + /// 登录用户 + /// + public string ITCode { get; set; } + + public string Name { get; set; } + + public string Memo { get; set; } + + public List Roles { get; set; } + /// + /// 用户的页面权限列表 + /// + public List FunctionPrivileges { get; set; } + + public List DataPrivileges { get; set; } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ListItem.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ListItem.cs new file mode 100644 index 0000000..169a493 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/ListItem.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.Linq; + +namespace WalkingTec.Mvvm.Core +{ + public class ListItem + { + /// + /// The value to display + /// + public string Text { get; set; } + + /// + /// The value to be submitted + /// + public object Value { get; set; } + + } + + public class MenuItem : ListItem + { + /// + /// Icon + /// + /// + public string Icon { get; set; } + } + + /// + /// 下拉菜单项 + /// + public class ComboSelectListItem : ListItem + { + /// + /// Whether it is selected + /// + public bool Selected { get; set; } + + /// + /// Whether it is disabled + /// + public bool Disabled { get; set; } + + /// + /// ParentId + /// + public string ParentId { get; set; } + + /// + /// Icon + /// + /// + public string Icon { get; set; } + + } + + /// + /// 树形下拉菜单项 + /// + public class TreeSelectListItem: ComboSelectListItem + { + public bool Expended { get; set; } + public string Url { get; set; } + public string Tag { get; set; } + public bool Leaf => Children == null || Children.Count() == 0; + public List Children { get; set; } + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/NugetInfo.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/NugetInfo.cs new file mode 100644 index 0000000..08439da --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/NugetInfo.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace WalkingTec.Mvvm.Core.Support +{ + public class NugetInfo + { + public List data { get; set; } + } + + public class NugetInfoData + { + public string version { get; set; } + public List versions {get;set;} + } + + public class NugetVersionData + { + public string version { get; set; } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/SupportedGroupMapping.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/SupportedGroupMapping.cs new file mode 100644 index 0000000..c6ca583 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/SupportedGroupMapping.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// DFS 业务类型与Group映射关系 + /// + public class SupportedBusinessTypeMapping + { + public string BusinessTypeName { get; set; } + public List GroupMappings { get; set; } + } + + /// + /// DFS Group与内外网地址映射关系 + /// + public class SupportedGroupMapping + { + /// + /// 群组名 + /// + public string Group { get; set; } + /// + /// 内网地址头 + /// + public string InsideUrlHeader { get; set; } + /// + /// 外网地址头 + /// + public string OutsideUrlHeader { get; set; } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/TypeComparer.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/TypeComparer.cs new file mode 100644 index 0000000..b36e631 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/TypeComparer.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// Type 类型比较器 + /// + public class TypeComparer : IEqualityComparer + { + public bool Equals(Type x, Type y) => x.AssemblyQualifiedName == y.AssemblyQualifiedName; + + public int GetHashCode(Type obj) => throw new NotImplementedException(); + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/WTMLogger.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/WTMLogger.cs new file mode 100644 index 0000000..ca5ffa8 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/WTMLogger.cs @@ -0,0 +1,149 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Configuration; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace WalkingTec.Mvvm.Core +{ + + public static class WTMeLoggerExtensions + { + public static ILoggingBuilder AddWTMLogger(this ILoggingBuilder builder) + { + builder.AddConfiguration(); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + return builder; + } + } + + public class WTMLoggerProvider : ILoggerProvider + { + private IServiceProvider sp = null; + private LoggerFilterOptions logConfig; + + public WTMLoggerProvider( IOptionsMonitor _logConfig, IServiceProvider sp) + { + this.sp = sp; + logConfig = _logConfig.CurrentValue; + } + + public ILogger CreateLogger(string categoryName) + { + return new WTMLogger(categoryName, logConfig,sp); + } + public void Dispose() { } + } + + public class WTMLogger : ILogger + { + private readonly string categoryName; + private IServiceProvider sp; + private LoggerFilterOptions logConfig; + + public WTMLogger(string categoryName, LoggerFilterOptions logConfig, IServiceProvider sp) + { + this.categoryName = categoryName; + this.logConfig = logConfig; + this.sp = sp; + } + + public bool IsEnabled(LogLevel logLevel) + { + if(logConfig == null) + { + return false; + } + var level = logConfig.Rules.Where(x => + x.ProviderName == "WTM" && + ( + (x.CategoryName != null && categoryName.ToLower().StartsWith(x.CategoryName.ToLower()) ) || + categoryName == "WalkingTec.Mvvm.Core.ActionLog" + ) + ) + .Select(x => x.LogLevel).FirstOrDefault(); + if (level == null) + { + level = LogLevel.Error; + } + if (logLevel >= level) + { + return true; + } + else + { + return false; + } + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (IsEnabled(logLevel)) + { + ActionLog log = null; + if (typeof(TState) != typeof(ActionLog)) + { + ActionLogTypesEnum ll = ActionLogTypesEnum.Normal; + if (logLevel == LogLevel.Trace || logLevel == LogLevel.Debug) + { + ll = ActionLogTypesEnum.Debug; + } + if (logLevel == LogLevel.Error || logLevel == LogLevel.Warning || logLevel == LogLevel.Critical) + { + ll = ActionLogTypesEnum.Exception; + } + + log = new ActionLog + { + Remark = formatter?.Invoke(state, exception), + CreateTime = DateTime.Now, + ActionTime = DateTime.Now, + ActionName = "WtmLog", + ModuleName = "WtmLog", + LogType = ll + }; + } + else + { + log = state as ActionLog; + } + + WTMContext wtm = null; + var hc = sp.GetRequiredService().HttpContext; + if (hc == null) + { + using (var scope = sp.CreateScope()) + { + wtm = scope.ServiceProvider.GetRequiredService(); + } + } + else + { + wtm = hc.RequestServices.GetRequiredService(); + } + if (wtm != null) + { + using (var dc = wtm.CreateDC(true)) + { + if (dc != null) + { + try + { + dc.AddEntity(log); + dc.SaveChanges(); + } + catch { } + } + } + } + } + } + + public IDisposable BeginScope(TState state) => null; + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/WebProxy.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/WebProxy.cs new file mode 100644 index 0000000..e7cdab5 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/WebProxy.cs @@ -0,0 +1,33 @@ +using System; +using System.Net; + +namespace WalkingTec.Mvvm.Core +{ + public class WebProxy : IWebProxy + { + public WebProxy(string proxyUri) + : this(new Uri(proxyUri)) + { + } + + public WebProxy(Uri proxyUri) + { + this.ProxyUri = proxyUri; + } + + public Uri ProxyUri { get; set; } + + public ICredentials Credentials { get; set; } + + public Uri GetProxy(Uri destination) + { + return this.ProxyUri; + } + + public bool IsBypassed(Uri host) + { + return false; /* Proxy all requests */ + } + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/WtmLocalizationOption.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/WtmLocalizationOption.cs new file mode 100644 index 0000000..8ad42a3 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Support/WtmLocalizationOption.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace WalkingTec.Mvvm.Core +{ + public class WtmLocalizationOption + { + public Type LocalizationType { get; set; } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Utils.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Utils.cs new file mode 100644 index 0000000..e6ad09e --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/Utils.cs @@ -0,0 +1,824 @@ +using Microsoft.Extensions.Caching.Distributed; +using NPOI.HSSF.Util; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using WalkingTec.Mvvm.Core.Extensions; +using WalkingTec.Mvvm.Core.Support; +using WalkingTec.Mvvm.Core.Support.Json; + +namespace WalkingTec.Mvvm.Core +{ + public class Utils + { + + private static List _allAssemblies; + + public static string GetCurrentComma() + { + if (CultureInfo.CurrentUICulture.Name == "zh-cn") + { + return ":"; + } + else + { + return ":"; + } + } + + public static List GetAllAssembly() + { + if (_allAssemblies == null) + { + _allAssemblies = new List(); + string path = null; + string singlefile = null; + try + { + path = Assembly.GetEntryAssembly()?.Location; + } + catch { } + if (string.IsNullOrEmpty(path)) + { + singlefile = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName; + path = Path.GetDirectoryName(singlefile); + } + var dir = new DirectoryInfo(Path.GetDirectoryName(path)); + + var dlls = dir.GetFiles("*.dll", SearchOption.TopDirectoryOnly); + string[] systemdll = new string[] + { + "Microsoft.", + "System.", + "Swashbuckle.", + "ICSharpCode", + "Newtonsoft.", + "Oracle.", + "Pomelo.", + "SQLitePCLRaw.", + "Aliyun.OSS", + "BouncyCastle.", + "FreeSql.", + "Google.Protobuf.dll", + "Humanizer.dll", + "IdleBus.dll", + "K4os.", + "MySql.Data.", + "Npgsql.", + "NPOI.", + "netstandard", + "MySqlConnector", + "VueCliMiddleware" + }; + + var filtered = dlls.Where(x => systemdll.Any(y => x.Name.StartsWith(y)) == false); + foreach (var dll in filtered) + { + try + { + AssemblyLoadContext.Default.LoadFromAssemblyPath(dll.FullName); + } + catch + { + } + } + var dlllist = AssemblyLoadContext.Default.Assemblies.Where(x => systemdll.Any(y => x.FullName.StartsWith(y)) == false).ToList(); + _allAssemblies.AddRange(dlllist); + } + return _allAssemblies; + } + + public static SimpleMenu FindMenu(string url, List menus) + { + if (url == null) + { + return null; + } + url = url.ToLower(); + if (menus == null) + { + return null; + } + //寻找菜单中是否有与当前判断的url完全相同的 + var menu = menus.Where(x => x.Url != null && x.Url.ToLower() == url).FirstOrDefault(); + + //如果没有,抹掉当前url的参数,用不带参数的url比对 + if (menu == null) + { + var pos = url.IndexOf("?"); + if (pos > 0) + { + url = url.Substring(0, pos); + menu = menus.Where(x => x.Url != null && (x.Url.ToLower() == url || x.Url.ToLower() + "async" == url)).FirstOrDefault(); + } + } + + //如果还没找到,则判断url是否为/controller/action/id这种格式,如果是则抹掉/id之后再对比 + if (menu == null && url.EndsWith("/index")) + { + url = url.Substring(0, url.Length - 6); + menu = menus.Where(x => x.Url != null && x.Url.ToLower() == url).FirstOrDefault(); + } + if (menu == null && url.EndsWith("/indexasync")) + { + url = url.Substring(0, url.Length - 11); + menu = menus.Where(x => x.Url != null && x.Url.ToLower() == url).FirstOrDefault(); + } + return menu; + } + + + public static string GetIdByName(string fieldName) + { + return fieldName == null ? "" : fieldName.Replace(".", "_").Replace("[", "_").Replace("]", "_").Replace("-","minus"); + } + + public static void CheckDifference(IEnumerable oldList, IEnumerable newList, out IEnumerable ToRemove, out IEnumerable ToAdd) where T : TopBasePoco + { + List tempToRemove = new List(); + List tempToAdd = new List(); + oldList = oldList ?? new List(); + newList = newList ?? new List(); + foreach (var oldItem in oldList) + { + bool exist = false; + foreach (var newItem in newList) + { + if (oldItem.GetID().ToString() == newItem.GetID().ToString()) + { + exist = true; + break; + } + } + if (exist == false) + { + tempToRemove.Add(oldItem); + } + } + foreach (var newItem in newList) + { + bool exist = false; + foreach (var oldItem in oldList) + { + if (newItem.GetID().ToString() == oldItem.GetID().ToString()) + { + exist = true; + break; + } + } + if (exist == false) + { + tempToAdd.Add(newItem); + } + } + ToRemove = tempToRemove.AsEnumerable(); + ToAdd = tempToAdd.AsEnumerable(); + } + + public static short GetExcelColor(string color) + { + var colors = typeof(HSSFColor).GetNestedTypes().ToList(); + foreach (var col in colors) + { + var pro = col.GetField("hexString"); + if (pro == null) + { + continue; + } + var hex = pro.GetValue(null); + var rgb = hex.ToString().Split(':'); + for (int i = 0; i < rgb.Length; i++) + { + if (rgb[i].Length > 2) + { + rgb[i] = rgb[i].Substring(0, 2); + } + } + int r = Convert.ToInt16(rgb[0], 16); + int g = Convert.ToInt16(rgb[1], 16); + int b = Convert.ToInt16(rgb[2], 16); + + if (color.Length == 8) + { + color = color.Substring(2); + } + string c1 = color.Substring(0, 2); + string c2 = color.Substring(2, 2); + string c3 = color.Substring(4, 2); + + int r1 = Convert.ToInt16(c1, 16); + int g1 = Convert.ToInt16(c2, 16); + int b1 = Convert.ToInt16(c3, 16); + + + if (r == r1 && g == g1 && b == b1) + { + return (short)col.GetField("index").GetValue(null); + } + } + return HSSFColor.COLOR_NORMAL; + } + + /// + /// 获取Bool类型的下拉框 + /// + /// + /// + /// + /// + /// + /// + public static List GetBoolCombo(BoolComboTypes boolType, bool? defaultValue = null, string trueText = null, string falseText = null, string selectText = null) + { + List rv = new List(); + string yesText = ""; + string noText = ""; + switch (boolType) + { + case BoolComboTypes.YesNo: + yesText = CoreProgram._localizer?["Sys.Yes"]; + noText = CoreProgram._localizer?["Sys.No"]; + break; + case BoolComboTypes.ValidInvalid: + yesText = CoreProgram._localizer?["Sys.Valid"]; + noText = CoreProgram._localizer?["Sys.Invalid"]; + break; + case BoolComboTypes.MaleFemale: + yesText = CoreProgram._localizer?["Sys.Male"]; + noText = CoreProgram._localizer?["Sys.Female"]; + break; + case BoolComboTypes.HaveNotHave: + yesText = CoreProgram._localizer?["Sys.Have"]; + noText = CoreProgram._localizer?["Sys.NotHave"]; + break; + case BoolComboTypes.Custom: + yesText = trueText ?? CoreProgram._localizer?["Sys.Yes"]; + noText = falseText ?? CoreProgram._localizer?["Sys.No"]; + break; + default: + break; + } + ComboSelectListItem yesItem = new ComboSelectListItem() + { + Text = yesText, + Value = "true" + }; + if (defaultValue == true) + { + yesItem.Selected = true; + } + ComboSelectListItem noItem = new ComboSelectListItem() + { + Text = noText, + Value = "false" + }; + if (defaultValue == false) + { + noItem.Selected = true; + } + if(selectText != null) + { + rv.Add(new ComboSelectListItem { Text = selectText, Value = "" }); + } + rv.Add(yesItem); + rv.Add(noItem); + return rv; + } + + + /// + /// + /// + /// + /// + public static string ZipAndBase64Encode(string input) + { + byte[] buffer = Encoding.UTF8.GetBytes(input); + MemoryStream inputms = new MemoryStream(buffer); + MemoryStream outputms = new MemoryStream(); + using (GZipStream zip = new GZipStream(outputms, CompressionMode.Compress)) + { + inputms.CopyTo(zip); + } + byte[] rv = outputms.ToArray(); + inputms.Dispose(); + return Convert.ToBase64String(rv); + } + /// + /// + /// + /// + /// + public static string UnZipAndBase64Decode(string input) + { + byte[] inputstr = Convert.FromBase64String(input); + MemoryStream inputms = new MemoryStream(inputstr); + MemoryStream outputms = new MemoryStream(); + using (GZipStream zip = new GZipStream(inputms, CompressionMode.Decompress)) + { + zip.CopyTo(outputms); + } + byte[] rv = outputms.ToArray(); + outputms.Dispose(); + return Encoding.UTF8.GetString(rv); + } + /// + /// + /// + /// + /// + public static string EncodeScriptJson(string input) + { + if (input == null) + { + return ""; + } + else + { + return input.Replace(Environment.NewLine, "").Replace("\"", "\\\\\\\"").Replace("'", "\\'"); + } + } + + /// + /// + /// + /// + public static void DeleteFile(string path) + { + try + { + System.IO.File.Delete(path); + } + catch { } + } + + #region 格式化文本 add by wuwh 2014.6.12 + /// + /// 格式化文本 + /// + /// 要格式化的字符串 + /// 是否是纯代码 + /// + public static string FormatText(string text, bool isCode = false) + { + + if (isCode) + { + return FormatCode(text); + } + else + { + #region 截取需要格式化的代码段 + List listInt = new List(); + int index = 0; + int _index; + while (true) + { + _index = text.IndexOf("&&", index); + index = _index + 1; + if (_index >= 0 && _index <= text.Length) + { + listInt.Add(_index); + } + else + { + break; + } + } + + List listStr = new List(); + for (int i = 0; i < listInt.Count; i++) + { + string temp = text.Substring(listInt[i] + 2, listInt[i + 1] - listInt[i] - 2); + + listStr.Add(temp); + i++; + } + #endregion + + #region 格式化代码段 + //先将 < >以及空格替换掉,防止下面替换出现 html标签后出现问题 + for (int i = 0; i < listStr.Count; i++) + { + //将 &&代码&& 替换成&&1&& + text = text.Replace("&&" + listStr[i] + "&&", FormatCode(listStr[i])); + } + #endregion + + return text; + } + } + #endregion + + #region 格式化代码 edit by wuwh + /// + /// 格式化代码 + /// + /// + /// + public static string FormatCode(string code) + { + //先将 < >以及空格替换掉,防止下面替换出现 html标签后出现问题 + code = code.Replace("<", "<").Replace(">", ">").Replace(" ", " "); + string csKeyWords = "abstract|as|base|bool|break|byte|case|catch|char|checked|class|const|continue|decimal|default|delegate|do|double|else|enum|event|explicit|extern|false|finally|fixed|float|for|foreach|from|get|goto|group|if|implicit|in|int|interface|internal|into|is|join|let|lock|long|namespace|new|null|object|operator|orderby|out|override|params|partial|private|protected|public|readonly|ref|return|sbyte|sealed|select|set|short|sizeof|stackalloc|static|string|struct|switch|this|throw|true|try|typeof|uint|ulong|unchecked|unsafe|ushort|using|value|var|virtual|void|volatile|where|while|yield"; + + string r1 = "(#if DBG[\\s\\S]+?#endif)"; + string r2 = "(#[a-z ]*)"; + string r3 = "(///\\ *<[/\\w]+>)"; + string r4 = "(/\\*[\\s\\S]*?\\*/)";//匹配三杠注释 + string r5 = "(//.*)";//匹配双杠注释 + string r6 = @"(@?"".*?"")";//匹配字符串 + string r7 = "('.*?')";//匹配字符串 + string r8 = "\\b(" + csKeyWords + ")\\b";//匹配关键字 + //string r9 = "class (.+) ";//匹配类 + //string r10 = "<(.+)>";//匹配泛型类 + + string rs = string.Format("{0}|{1}|{2}|{3}|{4}|{5}|{6}|{7}", r1, r2, r3, r4, r5, r6, r7, r8); + //string rs = string.Format("{0}|{1}|{2}|{3}|{4}|{5}|{6}|{7}|{8}|{9}", r1, r2, r3, r4, r5, r6, r7, r8, r9,r10); + + //$9$10 + string rr = "$1$2$3$4$5$6$7$8"; + + Regex re = new Regex(rs, RegexOptions.None); + code = Regex.Replace(code, rs, rr); + //替换换行符"\r\n" 以及"\r" "\n" + code = code.Replace("\r\n", "
").Replace("\n", "").Replace("\r", "
"); + //取消空标签 + //|C#类的颜色 + code = Regex.Replace(code, "|||", ""); + + return code; + } + #endregion + + #region 读取txt文件 + /// + /// 读取文件 + /// + /// 文件路径绝对 + /// + public static string ReadTxt(string path) + { + string result = string.Empty; + + if (File.Exists(path)) + { + using (Stream fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + using (TextReader sr = new StreamReader(fs, UnicodeEncoding.UTF8)) + { + result = sr.ReadToEnd(); + } + } + } + + return result; + } + #endregion + + /// + /// 得到目录下所有文件 + /// + /// + /// + public static List GetAllFileName(string dirpath) + { + DirectoryInfo dir = new DirectoryInfo(dirpath); + var files = dir.GetFileSystemInfos(); + return files.Select(x => x.Name).ToList(); + } + + #region add by wuwh 2014.10.18 递归获取目录下所有文件 + /// + /// 递归获取目录下所有文件 + /// + /// + /// + /// + public static List GetAllFilePathRecursion(string dirPath, List allFiles) + { + if (allFiles == null) + { + allFiles = new List(); + } + string[] subPaths = Directory.GetDirectories(dirPath); + foreach (var item in subPaths) + { + GetAllFilePathRecursion(item, allFiles); + } + allFiles.AddRange(Directory.GetFiles(dirPath).ToList()); + + return allFiles; + } + #endregion + + + /// + /// ConvertToColumnXType + /// + /// + /// + public static string ConvertToColumnXType(Type type) + { + if (type == typeof(bool) || type == typeof(bool?)) + { + return "checkcolumn"; + } + else if (type == typeof(DateTime) || type == typeof(DateTime?)) + { + return "datecolumn"; + } + else if (type == typeof(decimal) || type == typeof(decimal?) || type == typeof(double) || type == typeof(double?) || type == typeof(int) || type == typeof(int?) || type == typeof(long) || type == typeof(long?)) + { + return "numbercolumn"; + } + return "textcolumn"; + } + + + public static string GetCS(string cs, string mode, Configs config) + { + if (string.IsNullOrEmpty(cs) || config.Connections.Any(x => x.Key.ToLower() == cs.ToLower()) == false) + { + cs = "default"; + } + int index = cs.LastIndexOf("_"); + if (index > 0) + { + cs = cs.Substring(0, index); + } + if (mode?.ToLower() == "read") + { + var reads = config.Connections.Where(x => x.Key.StartsWith(cs + "_")).Select(x => x.Key).ToList(); + if (reads.Count > 0) + { + Random r = new Random(); + var v = r.Next(0, reads.Count); + cs = reads[v]; + } + } + return cs; + } + + public static string GetUrlByFileAttachmentId(IDataContext dc, Guid? fileAttachmentId, bool isIntranetUrl = false, string urlHeader = null) + { + string url = string.Empty; + var fileAttachment = dc.Set().Where(x => x.ID == fileAttachmentId.Value).FirstOrDefault(); + if (fileAttachment != null) + { + url = "/_Framework/GetFile/" + fileAttachmentId.ToString(); + + } + return url; + } + + #region 加解密 + /// + /// 通过密钥将内容加密 + /// + /// 要加密的字符串 + /// 加密密钥 + /// + public static string EncryptString(string stringToEncrypt, string encryptKey) + { + if (string.IsNullOrEmpty(stringToEncrypt)) + { + return ""; + } + + string stringEncrypted = string.Empty; + byte[] bytIn = UTF8Encoding.UTF8.GetBytes(stringToEncrypt); + MemoryStream encryptStream = new System.IO.MemoryStream(); + CryptoStream encStream = new CryptoStream(encryptStream, GenerateDESCryptoServiceProvider(encryptKey).CreateEncryptor(), CryptoStreamMode.Write); + + try + { + encStream.Write(bytIn, 0, bytIn.Length); + encStream.FlushFinalBlock(); + stringEncrypted = Convert.ToBase64String(encryptStream.ToArray(), 0, (int)encryptStream.Length); + } + catch + { + return ""; + } + finally + { + encryptStream.Close(); + encStream.Close(); + } + + return stringEncrypted; + } + + /// + /// 通过密钥讲内容解密 + /// + /// 要解密的字符串 + /// 密钥 + /// + public static string DecryptString(string stringToDecrypt, string encryptKey) + { + if (String.IsNullOrEmpty(stringToDecrypt)) + { + return ""; + } + + string stringDecrypted = string.Empty; + byte[] bytIn = Convert.FromBase64String(stringToDecrypt.Replace(" ", "+")); + MemoryStream decryptStream = new MemoryStream(); + CryptoStream encStream = new CryptoStream(decryptStream, GenerateDESCryptoServiceProvider(encryptKey).CreateDecryptor(), CryptoStreamMode.Write); + + try + { + encStream.Write(bytIn, 0, bytIn.Length); + encStream.FlushFinalBlock(); + stringDecrypted = Encoding.Default.GetString(decryptStream.ToArray()); + } + catch + { + return ""; + } + finally + { + decryptStream.Close(); + encStream.Close(); + } + + return stringDecrypted; + } + + private static DESCryptoServiceProvider GenerateDESCryptoServiceProvider(string key) + { + DESCryptoServiceProvider dCrypter = new DESCryptoServiceProvider(); + + string sTemp; + if (dCrypter.LegalKeySizes.Length > 0) + { + int moreSize = dCrypter.LegalKeySizes[0].MinSize; + while (key.Length > 8) + { + key = key.Substring(0, 8); + } + sTemp = key.PadRight(moreSize / 8, ' '); + } + else + { + sTemp = key; + } + byte[] bytKey = UTF8Encoding.UTF8.GetBytes(sTemp); + + dCrypter.Key = bytKey; + dCrypter.IV = bytKey; + + return dCrypter; + } + + #endregion + + #region MD5加密 + /// + /// 字符串MD5加密 + /// + /// + /// 返回大写32位MD5值 + public static string GetMD5String(string str) + { + byte[] buffer = Encoding.UTF8.GetBytes(str); + + return MD5String(buffer); + } + + /// + /// 流MD5加密 + /// + /// + /// + public static string GetMD5Stream(Stream stream) + { + byte[] buffer = new byte[stream.Length]; + stream.Read(buffer, 0, buffer.Length); + return MD5String(buffer); + } + + /// + /// 文件MD5加密 + /// + /// + /// 返回大写32位MD5值 + public static string GetMD5File(string path) + { + if (File.Exists(path)) + { + using (FileStream fs = new FileStream(path, FileMode.Open)) + { + return GetMD5Stream(fs); + } + } + else + { + return string.Empty; + } + } + + private static string MD5String(byte[] buffer) + { + MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider(); + byte[] cryptBuffer = md5.ComputeHash(buffer); + StringBuilder sb = new StringBuilder(); + foreach (byte item in cryptBuffer) + { + sb.Append(item.ToString("X2")); + } + return sb.ToString(); + } + #endregion + + /// + /// 重新处理 返回所有ispage模块 + /// + /// + /// 是否需要action + /// + public static List ResetModule(List modules, bool submit = true) + { + var m = modules.Select(x => new SimpleModule + { + ActionDes = x.ActionDes, + Actions = x.Actions.Select(y => new SimpleAction + { + ActionDes = y.ActionDes, + ActionName = y.ActionName, + Url = y.Url, + MethodName = y.MethodName, + IgnorePrivillege = y.IgnorePrivillege, + ID = y.ID, + Module = y.Module, + ModuleId = y.ModuleId, + Parameter = y.Parameter, + ParasToRunTest = y.ParasToRunTest + }).ToList(), + Area = x.Area, + AreaId = x.AreaId, + ClassName = x.ClassName, + _name = x._name, + ID = x.ID, + IgnorePrivillege = x.IgnorePrivillege, + IsApi = x.IsApi, + ModuleName = x.ModuleName, + NameSpace = x.NameSpace, + }).ToList(); + var mCount = m.Count; + var toRemove = new List(); + for (int i = 0; i < mCount; i++) + { + var pages = m[i].Actions?.Where(x => x.ActionDes?.IsPage == true).ToList(); + if (pages != null) + { + for (int j = 0; j < pages.Count; j++) + { + if (j == 0 && !m[i].Actions.Any(x => x.MethodName.ToLower() == "index")) + { + m.Add(new SimpleModule + { + ModuleName = pages[j].ActionDes._localizer[pages[j].ActionDes.Description], + NameSpace = m[i].NameSpace, + ClassName = pages[j].MethodName, + Actions = m[i].Actions, + Area = m[i].Area + }); + if (submit) + m[i].Actions.Remove(pages[j]); + toRemove.Add(m[i]); + } + else + { + if (pages[j].MethodName.ToLower() != "index") + { + m.Add(new SimpleModule + { + ModuleName = pages[j].ActionDes._localizer[pages[j].ActionDes.Description], + NameSpace = m[i].NameSpace, + ClassName = pages[j].MethodName, + Actions = submit ? new List() : new List() { pages[j] }, + Area = m[i].Area + }); + m[i].Actions.Remove(pages[j]); + } + } + } + } + } + toRemove.ForEach(x => m.Remove(x)); + return m; + } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/WTMContext.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/WTMContext.cs new file mode 100644 index 0000000..3dea8de --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/WTMContext.cs @@ -0,0 +1,966 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Net.Http; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Console; +using Microsoft.Extensions.Logging.Debug; +using Microsoft.Extensions.Options; +using WalkingTec.Mvvm.Core.Auth; +using WalkingTec.Mvvm.Core.Extensions; +using WalkingTec.Mvvm.Core.Json; +using WalkingTec.Mvvm.Core.Support.Json; + +namespace WalkingTec.Mvvm.Core +{ + public class WTMContext + { + private HttpContext _httpContext; + public HttpContext HttpContext { get => _httpContext; } + + private IServiceProvider _serviceProvider; + public IServiceProvider ServiceProvider { get => _serviceProvider ?? _httpContext?.RequestServices; } + + + private List _dps; + public List DataPrivilegeSettings { get => _dps; } + + private Configs _configInfo; + public Configs ConfigInfo { get => _configInfo; } + + private GlobalData _globaInfo; + public GlobalData GlobaInfo { get => _globaInfo; } + + private IUIService _uiservice; + public IUIService UIService { get => _uiservice; } + + private IDistributedCache _cache; + public IDistributedCache Cache + { + get + { + return _cache; + } + } + + public string CurrentCS { get; set; } + + public DBTypeEnum? CurrentDbType { get; set; } + + public string ParentWindowId + { + get + { + string rv = null; + if (WindowIds != null) + { + var ids = WindowIds.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + if (ids.Length > 1) + { + rv = ids[ids.Length - 2]; + } + } + + return rv ?? string.Empty; + } + } + + public string CurrentWindowId + { + get + { + string rv = null; + if (WindowIds != null) + { + var ids = WindowIds.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + if (ids.Length > 0) + { + rv = ids[ids.Length - 1]; + } + } + + return rv ?? string.Empty; + } + } + + public string WindowIds + { + get + { + string rv = string.Empty; + try + { + if (HttpContext.Request.Cookies.TryGetValue($"{ConfigInfo?.CookiePre}windowguid", out string windowguid) == true) + { + + if (HttpContext.Request.Cookies.TryGetValue($"{ConfigInfo?.CookiePre}{windowguid}windowids", out string windowid) == true) + { + rv = windowid; + } + } + } + catch { } + return rv; + } + } + + public ISessionService Session { get; set; } + + public IModelStateService MSD { get; set; } + + public static Func ReloadUserFunc { get; set; } + + #region DataContext + + private IDataContext _dc; + public IDataContext DC + { + get + { + if (_dc == null) + { + _dc = this.CreateDC(); + } + return _dc; + } + set + { + _dc = value; + } + } + + #endregion + + #region Current User + + private LoginUserInfo _loginUserInfo; + public LoginUserInfo LoginUserInfo + { + get + { + if (_loginUserInfo == null && HttpContext?.User?.Identity?.IsAuthenticated == true ) // 用户认证通过后,当前上下文不包含用户数据 + { + var userIdStr = HttpContext.User.Claims.Where(x => x.Type == AuthConstants.JwtClaimTypes.Subject).Select(x => x.Value).FirstOrDefault(); + var tenant = HttpContext.User.Claims.Where(x => x.Type == AuthConstants.JwtClaimTypes.TenantCode).Select(x=>x.Value).FirstOrDefault(); + string usercode = userIdStr; + var cacheKey = $"{GlobalConstants.CacheKey.UserInfo}:{userIdStr + "$`$" + tenant}"; + _loginUserInfo = Cache.Get(cacheKey); + if (_loginUserInfo == null) + { + try + { + _loginUserInfo = ReloadUser(usercode).Result; + } + catch { } + if (_loginUserInfo != null) + { + Cache.Add(cacheKey, _loginUserInfo); + } + else + { + return null; + } + } + } + return _loginUserInfo; + } + set + { + if (value == null) + { + Cache.Delete($"{GlobalConstants.CacheKey.UserInfo}:{_loginUserInfo?.ITCode + "$`$" + _loginUserInfo?.TenantCode}"); + _loginUserInfo = value; + } + else + { + _loginUserInfo = value; + Cache.Add($"{GlobalConstants.CacheKey.UserInfo}:{_loginUserInfo.ITCode+ "$`$" + _loginUserInfo?.TenantCode}", value); + } + } + } + + private Type _localizerType; + private IStringLocalizerFactory _stringLocalizerFactory; + private IStringLocalizer _localizer; + private ILoggerFactory _loggerFactory; + public IStringLocalizer Localizer + { + get + { + if (_localizer == null && _stringLocalizerFactory != null) + { + if(_localizerType == null) + { + _localizerType = Assembly.GetEntryAssembly().GetTypes().Where(x => x.Name == "Program").FirstOrDefault(); + } + _localizer = _stringLocalizerFactory.Create(_localizerType); + } + return _localizer ?? WalkingTec.Mvvm.Core.CoreProgram._localizer; + } + } + + /// + /// 从数据库读取用户 + /// + /// 用户名 + /// 用户信息 + public virtual async Task + ReloadUser(string itcode) + { + if (ReloadUserFunc != null) + { + var reload = ReloadUserFunc.Invoke(this, itcode); + if (reload != null) + { + return reload; + } + } + if (DC == null) + { + return null; + } + + + var code = await BaseUserQuery.Where(x => x.ITCode.ToLower() == itcode.ToLower()).Select(x =>new { itcode = x.ITCode, id= x.GetID(), photoid=x.PhotoId, name=x.Name }).SingleOrDefaultAsync(); + if (code == null) + { + return null; + } + LoginUserInfo rv = new LoginUserInfo + { + ITCode = code.itcode, + UserId = code.id?.ToString(), + Name = code.name, + PhotoId = code.photoid + }; + await rv.LoadBasicInfoAsync(this); + return rv; + } + + #endregion + + #region URL + public string BaseUrl { get; set; } + #endregion + + public SimpleLog Log { get; set; } + + protected ILogger Logger { get; set; } + + + private IQueryable _baseUserQuery; + public IQueryable BaseUserQuery + { + get + { + if(_baseUserQuery == null && this.GlobaInfo?.CustomUserType != null && DC != null) + { + var set = DC.GetType().GetMethod("Set", Type.EmptyTypes).MakeGenericMethod(GlobaInfo.CustomUserType); + _baseUserQuery = set.Invoke(DC, null) as IQueryable; + } + return _baseUserQuery; + } + } + + public WTMContext(IOptionsMonitor _config, GlobalData _gd = null, IHttpContextAccessor _http = null, IUIService _ui = null, List _dp = null, IDataContext dc = null, IStringLocalizerFactory stringLocalizer = null, ILoggerFactory loggerFactory = null, WtmLocalizationOption lop=null, IDistributedCache cache=null) + { + _configInfo = _config?.CurrentValue ?? new Configs(); + _globaInfo = _gd ?? new GlobalData(); + _httpContext = _http?.HttpContext; + _cache = cache; + _stringLocalizerFactory = stringLocalizer; + _loggerFactory = loggerFactory; + _localizerType = lop?.LocalizationType; + this.Logger = loggerFactory?.CreateLogger(); + if (_httpContext == null) + { + MSD = new BasicMSD(); + } + _uiservice = _ui; + if (_dp == null) + { + _dp = new List(); + } + _dps = _dp; + if (dc is NullContext) + { + _dc = null; + } + else + { + _dc = dc; + } + } + + public void SetServiceProvider(IServiceProvider sp) + { + this._serviceProvider = sp; + } + + + public T ReadFromCache(string key, Func setFunc, int? timeout = null) + { + if (Cache.TryGetValue(key, out T rv) == false || rv == null) + { + T data = setFunc(); + if (timeout == null) + { + Cache.Add(key, data); + } + else + { + Cache.Add(key, data, new DistributedCacheEntryOptions() + { + SlidingExpiration = new TimeSpan(0, 0, timeout.Value) + }); + } + return data; + } + else + { + return rv; + } + } + + public async Task RemoveUserCache( + params string[] userIds) + { + foreach (var userId in userIds) + { + var key = $"{GlobalConstants.CacheKey.UserInfo}:{userId+"$`$"+LoginUserInfo?.TenantCode}"; + await Cache.DeleteAsync(key); + } + } + + + #region CreateDC + public virtual IDataContext CreateDC(bool isLog = false, string cskey = null) + { + string cs = cskey ?? CurrentCS; + if (isLog == true) + { + if (ConfigInfo.Connections?.Where(x => x.Key.ToLower() == "defaultlog").FirstOrDefault() != null) + { + cs = "defaultlog"; + } + } + if (cs == null) + { + cs = "default"; + } + var rv = ConfigInfo.Connections.Where(x => x.Key.ToLower() == cs.ToLower()).FirstOrDefault().CreateDC(); + rv.IsDebug = ConfigInfo.IsQuickDebug; + rv.SetLoggerFactory(_loggerFactory); + return rv; + } + + #endregion + + /// + /// 判断某URL是否有权限访问 + /// + /// url地址 + /// true代表可以访问,false代表不能访问 + public bool IsAccessable(string url) + { + // 如果是调试 或者 url 为 null or 空字符串 + if (_configInfo.IsQuickDebug || string.IsNullOrEmpty(url) || IsUrlPublic(url)) + { + return true; + } + //循环所有不限制访问的url,如果含有当前判断的url,则认为可以访问 + var publicActions = _globaInfo.AllAccessUrls; + foreach (var au in publicActions) + { + if (new Regex("^"+au + "[/\\?]?", RegexOptions.IgnoreCase).IsMatch(url)) + { + return true; + } + } + //如果没有任何页面权限,则直接返回false + if (LoginUserInfo?.FunctionPrivileges == null) + { + return false; + } + + + url = Regex.Replace(url, "/do(batch.*)", "/$1", RegexOptions.IgnoreCase); + + //如果url以#开头,一般是javascript使用的临时地址,不需要判断,直接返回true + url = url.Trim(); + + if (url.StartsWith("#")) + { + return true; + } + var menus = _globaInfo.AllMenus; + var menu = Utils.FindMenu(url, GlobaInfo.AllMenus); + //如果最终没有找到,说明系统菜单中并没有配置这个url,返回false + if (menu == null) + { + return false; + } + //如果找到了,则继续验证其他权限 + else + { + return IsAccessable(menu, menus); + } + } + + /// + /// 判断某菜单是否有权限访问 + /// + /// 菜单项 + /// 所有系统菜单 + /// true代表可以访问,false代表不能访问 + protected bool IsAccessable(SimpleMenu menu, List menus) + { + //寻找当前菜单的页面权限 + var find = LoginUserInfo?.FunctionPrivileges.Where(x => x.MenuItemId == menu.ID && x.Allowed == true).FirstOrDefault(); + //如果能找到直接对应的页面权限 + if (find != null) + { + return true; + } + return false; + } + + public bool IsUrlPublic(string url) + { + var isPublic = false; + try + { + url = Regex.Replace(url, "/do(batch.*)", "/$1", RegexOptions.IgnoreCase); + url = url.Trim(); + + if (url.StartsWith("#")) + { + isPublic = true; + } + var menus = GlobaInfo.AllMenus; + var menu = Utils.FindMenu(url, menus); + if (menu != null && menu.IsPublic == true) + { + isPublic = true; + } + } + catch { } + return isPublic; + } + + public void DoLog(string msg, ActionLogTypesEnum logtype = ActionLogTypesEnum.Normal) + { + var log = this.Log?.GetActionLog(); + if (log == null) + { + log = new ActionLog(); + } + log.LogType = logtype; + log.ActionTime = DateTime.Now; + log.Remark = msg; + LogLevel ll = LogLevel.Information; + switch (logtype) + { + case ActionLogTypesEnum.Normal: + ll = LogLevel.Information; + break; + case ActionLogTypesEnum.Exception: + ll = LogLevel.Error; + break; + case ActionLogTypesEnum.Debug: + ll = LogLevel.Debug; + break; + default: + break; + } + + Logger?.Log(ll, new EventId(), log, null, (a, b) => + { + return $@" +===WTM Log=== +内容:{a.Remark} +地址:{a.ActionUrl} +时间:{a.ActionTime} +===WTM Log=== +"; + }); + } + + + #region CreateVM + /// + /// Create a ViewModel, and pass Session,cache,dc...etc to the viewmodel + /// + /// The type of the viewmodel + /// If the viewmodel is a BaseCRUDVM, the data having this id will be fetched + /// If the viewmodel is a BatchVM, the BatchVM's Ids property will be assigned + /// properties of the viewmodel that you want to assign values + /// if true, the viewmodel will not call InitVM internally + /// ViewModel + private BaseVM CreateVM(Type VMType, object Id = null, object[] Ids = null, Dictionary values = null, bool passInit = false) + { + //Use reflection to create viewmodel + var ctor = VMType.GetConstructor(Type.EmptyTypes); + BaseVM rv = ctor.Invoke(null) as BaseVM; + rv.Wtm = this; + + rv.FC = new Dictionary(); + rv.CreatorAssembly = this.GetType().AssemblyQualifiedName; + rv.ControllerName = this.HttpContext?.Request?.Path; + if (HttpContext != null && HttpContext.Request != null) + { + try + { + if (HttpContext.Request.QueryString != QueryString.Empty) + { + foreach (var key in HttpContext.Request.Query.Keys) + { + if (rv.FC.Keys.Contains(key) == false) + { + rv.FC.Add(key, HttpContext.Request.Query[key]); + } + } + } + if (HttpContext.Request.HasFormContentType) + { + var f = HttpContext.Request.Form; + foreach (var key in f.Keys) + { + if (rv.FC.Keys.Contains(key) == false) + { + rv.FC.Add(key, f[key]); + } + } + } + } + catch { } + } + //try to set values to the viewmodel's matching properties + if (values != null) + { + foreach (var v in values) + { + PropertyHelper.SetPropertyValue(rv, v.Key, v.Value, null, false); + } + } + //if viewmodel is derrived from BaseCRUDVM<> and Id has value, call ViewModel's GetById method + if (Id != null && rv is IBaseCRUDVM cvm) + { + cvm.SetEntityById(Id); + } + //if viewmodel is derrived from IBaseBatchVM<>,set ViewMode's Ids property,and init it's ListVM and EditModel properties + if (rv is IBaseBatchVM temp) + { + temp.Ids = new string[] { }; + if (Ids != null) + { + var tempids = new List(); + foreach (var iid in Ids) + { + tempids.Add(iid.ToString()); + } + temp.Ids = tempids.ToArray(); + } + if (temp.ListVM != null) + { + temp.ListVM.CopyContext(rv); + temp.ListVM.Ids = Ids == null ? new List() : temp.Ids.ToList(); + temp.ListVM.SearcherMode = ListVMSearchModeEnum.Batch; + temp.ListVM.NeedPage = false; + } + if (temp.LinkedVM != null) + { + temp.LinkedVM.CopyContext(rv); + } + if (temp.ListVM != null) + { + //Remove the action columns from list + temp.ListVM.OnAfterInitList += (self) => + { + self.RemoveActionColumn(); + self.RemoveAction(); + if (temp.ErrorMessage.Count > 0) + { + self.AddErrorColumn(); + } + }; + temp.ListVM.DoInitListVM(); + if (temp.ListVM.Searcher != null) + { + var searcher = temp.ListVM.Searcher; + searcher.CopyContext(rv); + if (passInit == false) + { + searcher.DoInit(); + } + } + } + temp.LinkedVM?.DoInit(); + //temp.ListVM.DoSearch(); + } + //if the viewmodel is a ListVM, Init it's searcher + if (rv is IBasePagedListVM lvm) + { + var searcher = lvm.Searcher; + searcher.CopyContext(rv); + if (passInit == false) + { + searcher.DoInit(); + } + lvm.DoInitListVM(); + + } + if (rv is IBaseImport tvm) + { + var template = tvm.Template; + template.CopyContext(rv); + template.DoInit(); + var errorlist = tvm.ErrorListVM; + errorlist.CopyContext(rv); + } + + //if passinit is not set, call the viewmodel's DoInit method + if (passInit == false) + { + rv.DoInit(); + } + return rv; + } + + /// + /// Create a ViewModel, and pass Session,cache,dc...etc to the viewmodel + /// + /// The type of the viewmodelThe type of the viewmodel + /// use Lambda to set viewmodel's properties,use && for multiply properties, for example Wtm.CreateVM(values: x=>x.Field1=='a' && x.Field2 == 'b'); will set viewmodel's Field1 to 'a' and Field2 to 'b' + /// if true, the viewmodel will not call InitVM internally + /// ViewModel + public T CreateVM(Expression> values = null, bool passInit = false) where T : BaseVM + { + SetValuesParser p = new SetValuesParser(); + var dir = p.Parse(values); + return CreateVM(typeof(T), null, new object[] { }, dir, passInit) as T; + } + + /// + /// Create a ViewModel, and pass Session,cache,dc...etc to the viewmodel + /// + /// The type of the viewmodelThe type of the viewmodel + /// If the viewmodel is a BaseCRUDVM, the data having this id will be fetched + /// properties of the viewmodel that you want to assign values + /// if true, the viewmodel will not call InitVM internally + /// ViewModel + public T CreateVM(object Id, Expression> values = null, bool passInit = false) where T : BaseVM + { + SetValuesParser p = new SetValuesParser(); + var dir = p.Parse(values); + return CreateVM(typeof(T), Id, new object[] { }, dir, passInit) as T; + } + + /// + /// Create a ViewModel, and pass Session,cache,dc...etc to the viewmodel + /// + /// The type of the viewmodelThe type of the viewmodel + /// If the viewmodel is a BatchVM, the BatchVM's Ids property will be assigned + /// use Lambda to set viewmodel's properties,use && for multiply properties, for example Wtm.CreateVM(values: x=>x.Field1=='a' && x.Field2 == 'b'); will set viewmodel's Field1 to 'a' and Field2 to 'b' + /// if true, the viewmodel will not call InitVM internally + /// ViewModel + public T CreateVM(object[] Ids, Expression> values = null, bool passInit = false) where T : BaseVM + { + SetValuesParser p = new SetValuesParser(); + var dir = p.Parse(values); + return CreateVM(typeof(T), null, Ids, dir, passInit) as T; + } + + + /// + /// Create a ViewModel, and pass Session,cache,dc...etc to the viewmodel + /// + /// The type of the viewmodelThe type of the viewmodel + /// If the viewmodel is a BatchVM, the BatchVM's Ids property will be assigned + /// use Lambda to set viewmodel's properties,use && for multiply properties, for example Wtm.CreateVM(values: x=>x.Field1=='a' && x.Field2 == 'b'); will set viewmodel's Field1 to 'a' and Field2 to 'b' + /// if true, the viewmodel will not call InitVM internally + /// ViewModel + public T CreateVM(Guid[] Ids, Expression> values = null, bool passInit = false) where T : BaseVM + { + SetValuesParser p = new SetValuesParser(); + var dir = p.Parse(values); + return CreateVM(typeof(T), null, Ids.Cast().ToArray(), dir, passInit) as T; + } + + /// + /// Create a ViewModel, and pass Session,cache,dc...etc to the viewmodel + /// + /// The type of the viewmodelThe type of the viewmodel + /// If the viewmodel is a BatchVM, the BatchVM's Ids property will be assigned + /// use Lambda to set viewmodel's properties,use && for multiply properties, for example Wtm.CreateVM(values: x=>x.Field1=='a' && x.Field2 == 'b'); will set viewmodel's Field1 to 'a' and Field2 to 'b' + /// if true, the viewmodel will not call InitVM internally + /// ViewModel + public T CreateVM(int[] Ids, Expression> values = null, bool passInit = false) where T : BaseVM + { + SetValuesParser p = new SetValuesParser(); + var dir = p.Parse(values); + return CreateVM(typeof(T), null, Ids.Cast().ToArray(), dir, passInit) as T; + } + + /// + /// Create a ViewModel, and pass Session,cache,dc...etc to the viewmodel + /// + /// The type of the viewmodelThe type of the viewmodel + /// If the viewmodel is a BatchVM, the BatchVM's Ids property will be assigned + /// use Lambda to set viewmodel's properties,use && for multiply properties, for example Wtm.CreateVM(values: x=>x.Field1=='a' && x.Field2 == 'b'); will set viewmodel's Field1 to 'a' and Field2 to 'b' + /// if true, the viewmodel will not call InitVM internally + /// ViewModel + public T CreateVM(long[] Ids, Expression> values = null, bool passInit = false) where T : BaseVM + { + SetValuesParser p = new SetValuesParser(); + var dir = p.Parse(values); + return CreateVM(typeof(T), null, Ids.Cast().ToArray(), dir, passInit) as T; + } + /// + /// Create a ViewModel, and pass Session,cache,dc...etc to the viewmodel + /// + /// The type of the viewmodelThe type of the viewmodel + /// If the viewmodel is a BatchVM, the BatchVM's Ids property will be assigned + /// use Lambda to set viewmodel's properties,use && for multiply properties, for example Wtm.CreateVM(values: x=>x.Field1=='a' && x.Field2 == 'b'); will set viewmodel's Field1 to 'a' and Field2 to 'b' + /// if true, the viewmodel will not call InitVM internally + /// ViewModel + public T CreateVM(string[] Ids, Expression> values = null, bool passInit = false) where T : BaseVM + { + SetValuesParser p = new SetValuesParser(); + var dir = p.Parse(values); + return CreateVM(typeof(T), null, Ids.Cast().ToArray(), dir, passInit) as T; + } + + /// + /// Create a ViewModel, and pass Session,cache,dc...etc to the viewmodel + /// + /// the fullname of the viewmodel's type + /// If the viewmodel is a BaseCRUDVM, the data having this id will be fetched + /// If the viewmodel is a BatchVM, the BatchVM's Ids property will be assigned + /// if true, the viewmodel will not call InitVM internally + /// ViewModel + public BaseVM CreateVM(string VmFullName, object Id = null, object[] Ids = null, bool passInit = false) + { + return CreateVM(Type.GetType(VmFullName), Id, Ids, null, passInit); + } + #endregion + + #region CallApi + public async Task> CallAPI(string domainName, string url, HttpMethodEnum method, HttpContent content, int? timeout = null, string proxy = null) where T:class + { + ApiResult rv = new ApiResult(); + try + { + var factory = this.ServiceProvider.GetRequiredService(); + if (string.IsNullOrEmpty(url)) + { + return rv; + } + //新建http请求 + HttpClient client = null; + if (string.IsNullOrEmpty(domainName)) + { + client = factory.CreateClient(); + } + else + { + client = factory.CreateClient(domainName); + } + //如果配置了代理,则使用代理 + //设置超时 + if (timeout.HasValue) + { + client.Timeout = new TimeSpan(0, 0, 0, timeout.Value, 0); + } + //填充表单数据 + HttpResponseMessage res = null; + switch (method) + { + case HttpMethodEnum.GET: + res = await client.GetAsync(url); + break; + case HttpMethodEnum.POST: + res = await client.PostAsync(url, content); + break; + case HttpMethodEnum.PUT: + res = await client.PutAsync(url, content); + break; + case HttpMethodEnum.DELETE: + res = await client.DeleteAsync(url); + break; + default: + break; + } + if (res == null) + { + return rv; + } + rv.StatusCode = res.StatusCode; + if (res.IsSuccessStatusCode == true) + { + Type dt = typeof(T); + if (dt == typeof(byte[])) + { + rv.Data = await res.Content.ReadAsByteArrayAsync() as T; + } + else + { + string responseTxt = await res.Content.ReadAsStringAsync(); + if (dt == typeof(string)) + { + rv.Data = responseTxt as T; + } + else + { + rv.Data = JsonSerializer.Deserialize(responseTxt, CoreProgram.DefaultJsonOption); + } + } + } + else + { + string responseTxt = await res.Content.ReadAsStringAsync(); + if (res.StatusCode == System.Net.HttpStatusCode.BadRequest) + { + + try + { + rv.Errors = JsonSerializer.Deserialize(responseTxt, CoreProgram.DefaultJsonOption); + } + catch { } + } + rv.ErrorMsg = responseTxt; + } + + return rv; + } + catch (Exception ex) + { + rv.ErrorMsg = ex.ToString(); + return rv; + } + } + + /// + /// 使用Get方法调用api + /// + /// + /// Appsettings中配置的Domain key + /// 调用地址 + /// 超时时间,单位秒 + /// 代理地址 + /// + public async Task> CallAPI(string domainName, string url, int? timeout = null, string proxy = null) where T : class + { + HttpContent content = null; + //填充表单数据 + return await CallAPI(domainName, url, HttpMethodEnum.GET, content, timeout, proxy); + } + + /// + /// + /// + /// + /// Appsettings中配置的Domain key + /// 调用地址 + /// 调用方式 + /// 提交字段 + /// 超时时间,单位秒 + /// 代理地址 + /// + public async Task> CallAPI(string domainName, string url, HttpMethodEnum method, IDictionary postdata, int? timeout = null, string proxy = null) where T : class + { + HttpContent content = null; + //填充表单数据 + if (!(postdata == null || postdata.Count == 0)) + { + List> paras = new List>(); + foreach (string key in postdata.Keys) + { + paras.Add(new KeyValuePair(key, postdata[key])); + } + content = new FormUrlEncodedContent(paras); + } + return await CallAPI(domainName, url, method, content, timeout, proxy); + } + + /// + /// + /// + /// + /// Appsettings中配置的Domain key + /// 调用地址 + /// 调用方式 + /// 提交的object,会被转成json提交 + /// 超时时间,单位秒 + /// 代理地址 + /// + public async Task> CallAPI(string domainName, string url, HttpMethodEnum method, object postdata, int? timeout = null, string proxy = null) where T : class + { + HttpContent content = new StringContent(JsonSerializer.Serialize(postdata, CoreProgram.DefaultPostJsonOption), System.Text.Encoding.UTF8, "application/json"); + return await CallAPI(domainName, url, method, content, timeout, proxy); + } + + public async Task> CallAPI(string domainName, string url, HttpMethodEnum method, HttpContent content, int? timeout = null, string proxy = null) + { + return await CallAPI(domainName, url, method, content, timeout, proxy); + } + + /// + /// 使用Get方法调用api + /// + /// Appsettings中配置的Domain key + /// 调用地址 + /// 超时时间,单位秒 + /// 代理地址 + /// + public async Task> CallAPI(string domainName, string url, int? timeout = null, string proxy = null) + { + return await CallAPI(domainName, url, timeout, proxy); + } + + /// + /// + /// + /// Appsettings中配置的Domain key + /// 调用地址 + /// 调用方式 + /// 提交字段 + /// 超时时间,单位秒 + /// 代理地址 + /// + public async Task> CallAPI(string domainName, string url, HttpMethodEnum method, IDictionary postdata,int? timeout = null, string proxy = null) + { + return await CallAPI(domainName, url, method, postdata, timeout, proxy); + + } + + /// + /// + /// + /// Appsettings中配置的Domain key + /// 调用地址 + /// 调用方式 + /// 提交的object,会被转成json提交 + /// 超时时间,单位秒 + /// 代理地址 + /// + public async Task> CallAPI(string domainName, string url, HttpMethodEnum method, object postdata, int? timeout = null, string proxy = null) + { + return await CallAPI(domainName, url, method, postdata, timeout, proxy); + } + + #endregion + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/WalkingTec.Mvvm.Core.csproj b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/WalkingTec.Mvvm.Core.csproj new file mode 100644 index 0000000..0fa8616 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Core/WalkingTec.Mvvm.Core.csproj @@ -0,0 +1,45 @@ + + + net5.0 + WalkingTec.Mvvm + WalkingTec.Mvvm.Core + $(AssemblyName) + true + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + + + + + + + diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Auth/Attribute/AuthorizeCookieAttribute.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Auth/Attribute/AuthorizeCookieAttribute.cs new file mode 100644 index 0000000..5c9b54f --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Auth/Attribute/AuthorizeCookieAttribute.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authorization; + +namespace WalkingTec.Mvvm.Mvc +{ + public class AuthorizeCookieAttribute : AuthorizeAttribute + { + public AuthorizeCookieAttribute() + { + AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme; + } + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Auth/Attribute/AuthorizeJwtAttribute.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Auth/Attribute/AuthorizeJwtAttribute.cs new file mode 100644 index 0000000..be1cbe5 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Auth/Attribute/AuthorizeJwtAttribute.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; + +namespace WalkingTec.Mvvm.Mvc +{ + public class AuthorizeJwtAttribute : AuthorizeAttribute + { + public AuthorizeJwtAttribute() + { + AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme; + } + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Auth/Attribute/AuthorizeJwtWithCookieAttribute.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Auth/Attribute/AuthorizeJwtWithCookieAttribute.cs new file mode 100644 index 0000000..e6acb88 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Auth/Attribute/AuthorizeJwtWithCookieAttribute.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; + +namespace WalkingTec.Mvvm.Mvc +{ + public class AuthorizeJwtWithCookieAttribute : AuthorizeAttribute + { + public AuthorizeJwtWithCookieAttribute() + { + AuthenticationSchemes = $"{CookieAuthenticationDefaults.AuthenticationScheme},{JwtBearerDefaults.AuthenticationScheme}"; + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Auth/JwtAuth/ITokenService.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Auth/JwtAuth/ITokenService.cs new file mode 100644 index 0000000..9f2dcfa --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Auth/JwtAuth/ITokenService.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; +using WalkingTec.Mvvm.Core; + +namespace WalkingTec.Mvvm.Mvc.Auth +{ + public interface ITokenService + { + IDataContext DC { get; } + + /// + /// Issue token + /// + /// + /// + Task IssueTokenAsync(LoginUserInfo loginUserInfo); + + /// + /// refresh token + /// + /// refreshToken + /// + Task RefreshTokenAsync(string refreshToken); + + /// + /// clear expired refresh tokens + /// + /// + Task ClearExpiredRefreshTokenAsync(); + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Auth/JwtAuth/Token.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Auth/JwtAuth/Token.cs new file mode 100644 index 0000000..0a3a258 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Auth/JwtAuth/Token.cs @@ -0,0 +1,20 @@ + +using System.Text.Json.Serialization; + +namespace WalkingTec.Mvvm.Mvc.Auth +{ + public class Token + { + [JsonPropertyName("access_token")] + public string AccessToken { get; set; } + + [JsonPropertyName("expires_in")] + public int ExpiresIn { get; set; } + + [JsonPropertyName("token_type")] + public string TokenType { get; set; } + + [JsonPropertyName("refresh_token")] + public string RefreshToken { get; set; } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Auth/JwtAuth/TokenService.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Auth/JwtAuth/TokenService.cs new file mode 100644 index 0000000..1dc61f4 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Auth/JwtAuth/TokenService.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using WalkingTec.Mvvm.Core; +using WalkingTec.Mvvm.Core.Auth; +using WalkingTec.Mvvm.Core.Extensions; + +namespace WalkingTec.Mvvm.Mvc.Auth +{ + public class TokenService : ITokenService + { + private readonly ILogger _logger; + private readonly JwtOption _jwtOptions; + + private const Token _emptyToken = null; + + private readonly Configs _configs; + private readonly IDataContext _dc; + public IDataContext DC => _dc; + + public TokenService( + ILogger logger, + IOptionsMonitor configs + ) + { + _configs = configs.CurrentValue; + _jwtOptions = _configs.JwtOptions; + _logger = logger; + _dc = CreateDC(); + } + + public async Task IssueTokenAsync(LoginUserInfo loginUserInfo) + { + if (loginUserInfo == null) + throw new ArgumentNullException(nameof(loginUserInfo)); + + var signinCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.SecurityKey)), SecurityAlgorithms.HmacSha256); + + + var cls = new List() + { + new Claim(AuthConstants.JwtClaimTypes.Subject, loginUserInfo.ITCode.ToString()) + }; + if (string.IsNullOrEmpty(loginUserInfo.TenantCode) == false) + { + cls.Add(new Claim(AuthConstants.JwtClaimTypes.TenantCode, loginUserInfo.TenantCode.ToString())); + } + var tokeOptions = new JwtSecurityToken( + issuer: _jwtOptions.Issuer, + audience: _jwtOptions.Audience, + claims: cls, + expires: DateTime.Now.AddSeconds(_jwtOptions.Expires), + signingCredentials: signinCredentials + ); + + var refreshToken = new PersistedGrant() + { + UserCode = loginUserInfo.ITCode + "$`$" + loginUserInfo.TenantCode, + Type = "refresh_token", + CreationTime = DateTime.Now, + RefreshToken = Guid.NewGuid().ToString("N"), + Expiration = DateTime.Now.AddSeconds(_jwtOptions.RefreshTokenExpires) + }; + _dc.AddEntity(refreshToken); + await _dc.SaveChangesAsync(); + + var token = new JwtSecurityTokenHandler().WriteToken(tokeOptions); + + return await Task.FromResult(new Token() + { + AccessToken = token, + ExpiresIn = _jwtOptions.Expires, + TokenType = AuthConstants.JwtTokenType, + RefreshToken = refreshToken.RefreshToken + }); + } + + private IDataContext CreateDC() + { + string cs = "tokendefault"; + if (_configs.Connections.Any(x => x.Key.ToLower() == cs) == false) + { + cs = "default"; + } + return _configs.Connections.Where(x => x.Key.ToLower() == cs).FirstOrDefault().CreateDC(); + } + + + /// + /// refresh token + /// + /// refreshToken + /// + public async Task RefreshTokenAsync(string refreshToken) + { + // 获取 RefreshToken + PersistedGrant persistedGrant = await _dc.Set().Where(x => x.RefreshToken == refreshToken).SingleOrDefaultAsync(); + if (persistedGrant != null) + { + // 校验 regresh token 有效期 + if (persistedGrant.Expiration < DateTime.Now) + throw new Exception("refresh token 已过期"); + + // 删除 refresh token + _dc.DeleteEntity(persistedGrant); + await _dc.SaveChangesAsync(); + + var pair = persistedGrant.UserCode?.Split("$`$"); + //生成并返回登录用户信息 + var loginUserInfo = new LoginUserInfo() + { + ITCode = pair[0], + TenantCode = pair[1], + }; + + // 清理过期 refreshtoken + var sql = $"DELETE FROM {DC.GetTableName()} WHERE Expiration<'{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}'"; + _dc.RunSQL(sql); + _logger.LogDebug("清理过期的refreshToken:【sql:{0}】", sql); + + // 颁发 token + return await IssueTokenAsync(loginUserInfo); + } + else + { + return null; + } + } + + /// + /// clear expired refresh tokens + /// + /// + public async Task ClearExpiredRefreshTokenAsync() + { + var dataTime = DateTime.Now; + var mapping = _dc.GetTableName(); + var sql = $"DELETE FROM {mapping} WHERE Expiration<=@dataTime"; + _dc.RunSQL(sql, new + { + dataTime = dataTime + }); + await Task.CompletedTask; + } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/BaseApiController.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/BaseApiController.cs new file mode 100644 index 0000000..78aaf86 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/BaseApiController.cs @@ -0,0 +1,313 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using WalkingTec.Mvvm.Core; +using WalkingTec.Mvvm.Core.Auth; +using WalkingTec.Mvvm.Core.Extensions; +using WalkingTec.Mvvm.Core.Implement; +using WalkingTec.Mvvm.Core.Support.Json; + +namespace WalkingTec.Mvvm.Mvc +{ + public abstract class BaseApiController : ControllerBase, IBaseController + { + + [JsonIgnore] + [BindNever] + public WTMContext Wtm { get; set; } + + [JsonIgnore] + [BindNever] + public Configs ConfigInfo { get => Wtm?.ConfigInfo; } + + [JsonIgnore] + [BindNever] + public GlobalData GlobaInfo { get => Wtm?.GlobaInfo; } + + + [JsonIgnore] + [BindNever] + public IDistributedCache Cache { get => Wtm?.Cache; } + + [JsonIgnore] + [BindNever] + public string CurrentCS { get => Wtm?.CurrentCS; } + + [JsonIgnore] + [BindNever] + public DBTypeEnum? CurrentDbType { get => Wtm?.CurrentDbType; } + + [JsonIgnore] + [BindNever] + public IDataContext DC { get => Wtm?.DC; } + + [JsonIgnore] + [BindNever] + public string BaseUrl { get => Wtm?.BaseUrl; } + [JsonIgnore] + [BindNever] + public IStringLocalizer Localizer { get => Wtm?.Localizer; } + + + //-------------------------------------------方法------------------------------------// + + //#region CreateVM + ///// + ///// Create a ViewModel, and pass Session,cache,dc...etc to the viewmodel + ///// + ///// The type of the viewmodel + ///// If the viewmodel is a BaseCRUDVM, the data having this id will be fetched + ///// If the viewmodel is a BatchVM, the BatchVM's Ids property will be assigned + ///// properties of the viewmodel that you want to assign values + ///// if true, the viewmodel will not call InitVM internally + ///// ViewModel + //[NonAction] + //private BaseVM CreateVM(Type VMType, object Id = null, object[] Ids = null, Dictionary values = null, bool passInit = false) + //{ + // //通过反射创建ViewModel并赋值 + // var ctor = VMType.GetConstructor(Type.EmptyTypes); + // BaseVM rv = ctor.Invoke(null) as BaseVM; + // rv.Wtm = Wtm; + // rv.FC = new Dictionary(); + // rv.CreatorAssembly = this.GetType().AssemblyQualifiedName; + // rv.ControllerName = this.GetType().FullName; + // if (HttpContext != null && HttpContext.Request != null) + // { + // try + // { + // if (Request.QueryString != QueryString.Empty) + // { + // foreach (var key in Request.Query.Keys) + // { + // if (rv.FC.Keys.Contains(key) == false) + // { + // rv.FC.Add(key, Request.Query[key]); + // } + // } + // } + // var f = HttpContext.Request.Form; + // foreach (var key in f.Keys) + // { + // if (rv.FC.Keys.Contains(key) == false) + // { + // rv.FC.Add(key, f[key]); + // } + // } + // } + // catch { } + // } + // //如果传递了默认值,则给vm赋值 + // if (values != null) + // { + // foreach (var v in values) + // { + // PropertyHelper.SetPropertyValue(rv, v.Key, v.Value, null, false); + // } + // } + // //如果ViewModel T继承自BaseCRUDVM<>且Id有值,那么自动调用ViewModel的GetById方法 + // if (Id != null && rv is IBaseCRUDVM cvm) + // { + // cvm.SetEntityById(Id); + // } + // //如果ViewModel T继承自IBaseBatchVM,则自动为其中的ListVM和EditModel初始化数据 + // if (rv is IBaseBatchVM temp) + // { + // temp.Ids = new string[] { }; + // if (Ids != null) + // { + // var tempids = new List(); + // foreach (var iid in Ids) + // { + // tempids.Add(iid.ToString()); + // } + // temp.Ids = tempids.ToArray(); + // } + // if (temp.ListVM != null) + // { + // temp.ListVM.CopyContext(rv); + // temp.ListVM.Ids = Ids == null ? new List() : temp.Ids.ToList(); + // temp.ListVM.SearcherMode = ListVMSearchModeEnum.Batch; + // temp.ListVM.NeedPage = false; + // } + // if (temp.LinkedVM != null) + // { + // temp.LinkedVM.CopyContext(rv); + // } + // if (temp.ListVM != null) + // { + // //绑定ListVM的OnAfterInitList事件,当ListVM的InitList完成时,自动将操作列移除 + // temp.ListVM.OnAfterInitList += (self) => + // { + // self.RemoveActionColumn(); + // self.RemoveAction(); + // if (temp.ErrorMessage.Count > 0) + // { + // self.AddErrorColumn(); + // } + // }; + // if (temp.ListVM.Searcher != null) + // { + // var searcher = temp.ListVM.Searcher; + // searcher.CopyContext(rv); + // if (passInit == false) + // { + // searcher.DoInit(); + // } + // } + // } + // temp.LinkedVM?.DoInit(); + // //temp.ListVM?.DoSearch(); + // } + // //如果ViewModel是ListVM,则初始化Searcher并调用Searcher的InitVM方法 + // if (rv is IBasePagedListVM lvm) + // { + // var searcher = lvm.Searcher; + // searcher.CopyContext(rv); + // if (passInit == false) + // { + // //获取保存在Cookie中的搜索条件的值,并自动给Searcher中的对应字段赋值 + // string namePre = ConfigInfo.CookiePre + "`Searcher" + "`" + rv.VMFullName + "`"; + // Type searcherType = searcher.GetType(); + // var pros = searcherType.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance).ToList(); + // pros.Add(searcherType.GetProperty("IsValid")); + + // searcher.DoInit(); + // } + // } + // if (rv is IBaseImport tvm) + // { + // var template = tvm.Template; + // template.CopyContext(rv); + // template.DoInit(); + // } + + // //自动调用ViewMode的InitVM方法 + // if (passInit == false) + // { + // rv.DoInit(); + // } + // return rv; + //} + + ///// + ///// Create a ViewModel, and pass Session,cache,dc...etc to the viewmodel + ///// + ///// The type of the viewmodel + ///// If the viewmodel is a BaseCRUDVM, the data having this id will be fetched + ///// If the viewmodel is a BatchVM, the BatchVM's Ids property will be assigned + ///// use Lambda to set viewmodel's properties,use && for multiply properties, for example Wtm.CreateVM(values: x=>x.Field1=='a' && x.Field2 == 'b'); will set viewmodel's Field1 to 'a' and Field2 to 'b' + ///// if true, the viewmodel will not call InitVM internally + ///// ViewModel + //[NonAction] + //public T Wtm.CreateVM(object Id = null, object[] Ids = null, Expression> values = null, bool passInit = false) where T : BaseVM + //{ + // SetValuesParser p = new SetValuesParser(); + // var dir = p.Parse(values); + // return CreateVM(typeof(T), Id, Ids, dir, passInit) as T; + //} + + ///// + ///// Create a ViewModel, and pass Session,cache,dc...etc to the viewmodel + ///// + ///// the fullname of the viewmodel's type + ///// If the viewmodel is a BaseCRUDVM, the data having this id will be fetched + ///// If the viewmodel is a BatchVM, the BatchVM's Ids property will be assigned + ///// if true, the viewmodel will not call InitVM internally + ///// ViewModel + //[NonAction] + //public BaseVM CreateVM(string VmFullName, object Id = null, object[] Ids = null, bool passInit = false) + //{ + // return CreateVM(Type.GetType(VmFullName), Id, Ids, null, passInit); + //} + //#endregion + + + #region ReInit model + [NonAction] + private void SetReInit(ModelStateDictionary msd, BaseVM model) + { + var reinit = model.GetType().GetTypeInfo().GetCustomAttributes(typeof(ReInitAttribute), false).Cast().SingleOrDefault(); + + if (ModelState.IsValid) + { + if (reinit != null && (reinit.ReInitMode == ReInitModes.SUCCESSONLY || reinit.ReInitMode == ReInitModes.ALWAYS)) + { + model.DoReInit(); + } + } + else + { + if (reinit == null || (reinit.ReInitMode == ReInitModes.FAILEDONLY || reinit.ReInitMode == ReInitModes.ALWAYS)) + { + model.DoReInit(); + } + } + } + #endregion + + #region Validate model + [NonAction] + public Dictionary RedoValidation(object item) + { + Dictionary rv = new Dictionary(); + TryValidateModel(item); + + foreach (var e in ControllerContext.ModelState) + { + if (e.Value.ValidationState == ModelValidationState.Invalid) + { + rv.Add(e.Key, e.Value.Errors.Select(x => x.ErrorMessage).ToSepratedString()); + } + } + + return rv; + } + #endregion + + #region update viewmodel + /// + /// Set viewmodel's properties to the matching items posted by user + /// + /// ViewModel + /// prefix + /// true if success + [NonAction] + public bool RedoUpdateModel(object vm, string prefix = null) + { + try + { + BaseVM bvm = vm as BaseVM; + foreach (var item in bvm.FC.Keys) + { + PropertyHelper.SetPropertyValue(vm, item, bvm.FC[item], prefix, true); + } + return true; + } + catch + { + return false; + } + } + #endregion + + protected JsonResult JsonMore(object data, int statusCode = StatusCodes.Status200OK, string msg = "success") + { + return new JsonResult(new JsonResultT { Msg = msg, Code = statusCode, Data = data }); + } + + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/BaseController.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/BaseController.cs new file mode 100644 index 0000000..c89f1c8 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/BaseController.cs @@ -0,0 +1,435 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using WalkingTec.Mvvm.Core; +using WalkingTec.Mvvm.Core.Auth; +using WalkingTec.Mvvm.Core.Extensions; +using WalkingTec.Mvvm.Core.Support.Json; + +namespace WalkingTec.Mvvm.Mvc +{ + public abstract class BaseController : Controller, IBaseController + { + [JsonIgnore] + [BindNever] + public WTMContext Wtm { get; set; } + + + [JsonIgnore] + [BindNever] + public Configs ConfigInfo { get => Wtm?.ConfigInfo; } + + [JsonIgnore] + [BindNever] + public GlobalData GlobaInfo { get => Wtm?.GlobaInfo; } + + [JsonIgnore] + [BindNever] + public IUIService UIService { get => Wtm?.UIService; } + + [JsonIgnore] + [BindNever] + public IDistributedCache Cache { get => Wtm?.Cache; } + + [JsonIgnore] + [BindNever] + public string CurrentCS { get => Wtm?.CurrentCS; } + + [JsonIgnore] + [BindNever] + public DBTypeEnum? CurrentDbType { get => Wtm?.CurrentDbType; } + + [JsonIgnore] + [BindNever] + public string ParentWindowId { get => Wtm?.ParentWindowId; } + + [JsonIgnore] + [BindNever] + public string CurrentWindowId { get => Wtm?.CurrentWindowId; } + + [JsonIgnore] + [BindNever] + public string WindowIds { get => Wtm?.WindowIds; } + + #region DataContext + + [JsonIgnore] + [BindNever] + public IDataContext DC { get => Wtm?.DC; } + + #endregion + + #region URL + [JsonIgnore] + [BindNever] + public string BaseUrl { get => Wtm?.BaseUrl; } + #endregion + + [JsonIgnore] + [BindNever] + public IStringLocalizer Localizer { get => Wtm?.Localizer; } + + + //-------------------------------------------方法------------------------------------// + + //#region CreateVM + ///// + ///// Create a ViewModel, and pass Session,cache,dc...etc to the viewmodel + ///// + ///// The type of the viewmodel + ///// If the viewmodel is a BaseCRUDVM, the data having this id will be fetched + ///// If the viewmodel is a BatchVM, the BatchVM's Ids property will be assigned + ///// properties of the viewmodel that you want to assign values + ///// if true, the viewmodel will not call InitVM internally + ///// ViewModel + //private BaseVM CreateVM(Type VMType, object Id = null, object[] Ids = null, Dictionary values = null, bool passInit = false) + //{ + // //Use reflection to create viewmodel + // var ctor = VMType.GetConstructor(Type.EmptyTypes); + // BaseVM rv = ctor.Invoke(null) as BaseVM; + // rv.Wtm = this.Wtm; + + // rv.FC = new Dictionary(); + // rv.CreatorAssembly = this.GetType().AssemblyQualifiedName; + // rv.Controller = this; + // rv.ControllerName = this.GetType().FullName; + // if (HttpContext != null && HttpContext.Request != null) + // { + // try + // { + // if (Request.QueryString != QueryString.Empty) + // { + // foreach (var key in Request.Query.Keys) + // { + // if (rv.FC.Keys.Contains(key) == false) + // { + // rv.FC.Add(key, Request.Query[key]); + // } + // } + // } + // if (HttpContext.Request.HasFormContentType) + // { + // var f = HttpContext.Request.Form; + // foreach (var key in f.Keys) + // { + // if (rv.FC.Keys.Contains(key) == false) + // { + // rv.FC.Add(key, f[key]); + // } + // } + // } + // } + // catch { } + // } + // //try to set values to the viewmodel's matching properties + // if (values != null) + // { + // foreach (var v in values) + // { + // PropertyHelper.SetPropertyValue(rv, v.Key, v.Value, null, false); + // } + // } + // //if viewmodel is derrived from BaseCRUDVM<> and Id has value, call ViewModel's GetById method + // if (Id != null && rv is IBaseCRUDVM cvm) + // { + // cvm.SetEntityById(Id); + // } + // //if viewmodel is derrived from IBaseBatchVM<>,set ViewMode's Ids property,and init it's ListVM and EditModel properties + // if (rv is IBaseBatchVM temp) + // { + // temp.Ids = new string[] { }; + // if (Ids != null) + // { + // var tempids = new List(); + // foreach (var iid in Ids) + // { + // tempids.Add(iid.ToString()); + // } + // temp.Ids = tempids.ToArray(); + // } + // if (temp.ListVM != null) + // { + // temp.ListVM.CopyContext(rv); + // temp.ListVM.Ids = Ids == null ? new List() : temp.Ids.ToList(); + // temp.ListVM.SearcherMode = ListVMSearchModeEnum.Batch; + // temp.ListVM.NeedPage = false; + // } + // if (temp.LinkedVM != null) + // { + // temp.LinkedVM.CopyContext(rv); + // } + // if (temp.ListVM != null) + // { + // //Remove the action columns from list + // temp.ListVM.OnAfterInitList += (self) => + // { + // self.RemoveActionColumn(); + // self.RemoveAction(); + // if (temp.ErrorMessage.Count > 0) + // { + // self.AddErrorColumn(); + // } + // }; + // temp.ListVM.DoInitListVM(); + // if (temp.ListVM.Searcher != null) + // { + // var searcher = temp.ListVM.Searcher; + // searcher.CopyContext(rv); + // if (passInit == false) + // { + // searcher.DoInit(); + // } + // } + // } + // temp.LinkedVM?.DoInit(); + // //temp.ListVM.DoSearch(); + // } + // //if the viewmodel is a ListVM, Init it's searcher + // if (rv is IBasePagedListVM lvm) + // { + // var searcher = lvm.Searcher; + // searcher.CopyContext(rv); + // if (passInit == false) + // { + // searcher.DoInit(); + // } + // lvm.DoInitListVM(); + + // } + // if (rv is IBaseImport tvm) + // { + // var template = tvm.Template; + // template.CopyContext(rv); + // template.DoInit(); + // } + + // //if passinit is not set, call the viewmodel's DoInit method + // if (passInit == false) + // { + // rv.DoInit(); + // } + // return rv; + //} + + ///// + ///// Create a ViewModel, and pass Session,cache,dc...etc to the viewmodel + ///// + ///// The type of the viewmodelThe type of the viewmodel + ///// use Lambda to set viewmodel's properties,use && for multiply properties, for example Wtm.CreateVM(values: x=>x.Field1=='a' && x.Field2 == 'b'); will set viewmodel's Field1 to 'a' and Field2 to 'b' + ///// if true, the viewmodel will not call InitVM internally + ///// ViewModel + //public T Wtm.CreateVM(Expression> values = null, bool passInit = false) where T : BaseVM + //{ + // SetValuesParser p = new SetValuesParser(); + // var dir = p.Parse(values); + // return CreateVM(typeof(T), null, new object[] { }, dir, passInit) as T; + //} + + ///// + ///// Create a ViewModel, and pass Session,cache,dc...etc to the viewmodel + ///// + ///// The type of the viewmodelThe type of the viewmodel + ///// If the viewmodel is a BaseCRUDVM, the data having this id will be fetched + ///// properties of the viewmodel that you want to assign values + ///// if true, the viewmodel will not call InitVM internally + ///// ViewModel + //public T Wtm.CreateVM(object Id, Expression> values = null, bool passInit = false) where T : BaseVM + //{ + // SetValuesParser p = new SetValuesParser(); + // var dir = p.Parse(values); + // return CreateVM(typeof(T), Id, new object[] { }, dir, passInit) as T; + //} + + ///// + ///// Create a ViewModel, and pass Session,cache,dc...etc to the viewmodel + ///// + ///// The type of the viewmodelThe type of the viewmodel + ///// If the viewmodel is a BatchVM, the BatchVM's Ids property will be assigned + ///// use Lambda to set viewmodel's properties,use && for multiply properties, for example Wtm.CreateVM(values: x=>x.Field1=='a' && x.Field2 == 'b'); will set viewmodel's Field1 to 'a' and Field2 to 'b' + ///// if true, the viewmodel will not call InitVM internally + ///// ViewModel + //public T Wtm.CreateVM(object[] Ids, Expression> values = null, bool passInit = false) where T : BaseVM + //{ + // SetValuesParser p = new SetValuesParser(); + // var dir = p.Parse(values); + // return CreateVM(typeof(T), null, Ids, dir, passInit) as T; + //} + + + ///// + ///// Create a ViewModel, and pass Session,cache,dc...etc to the viewmodel + ///// + ///// The type of the viewmodelThe type of the viewmodel + ///// If the viewmodel is a BatchVM, the BatchVM's Ids property will be assigned + ///// use Lambda to set viewmodel's properties,use && for multiply properties, for example Wtm.CreateVM(values: x=>x.Field1=='a' && x.Field2 == 'b'); will set viewmodel's Field1 to 'a' and Field2 to 'b' + ///// if true, the viewmodel will not call InitVM internally + ///// ViewModel + //public T Wtm.CreateVM(Guid[] Ids, Expression> values = null, bool passInit = false) where T : BaseVM + //{ + // SetValuesParser p = new SetValuesParser(); + // var dir = p.Parse(values); + // return CreateVM(typeof(T), null, Ids.Cast().ToArray(), dir, passInit) as T; + //} + + ///// + ///// Create a ViewModel, and pass Session,cache,dc...etc to the viewmodel + ///// + ///// The type of the viewmodelThe type of the viewmodel + ///// If the viewmodel is a BatchVM, the BatchVM's Ids property will be assigned + ///// use Lambda to set viewmodel's properties,use && for multiply properties, for example Wtm.CreateVM(values: x=>x.Field1=='a' && x.Field2 == 'b'); will set viewmodel's Field1 to 'a' and Field2 to 'b' + ///// if true, the viewmodel will not call InitVM internally + ///// ViewModel + //public T Wtm.CreateVM(int[] Ids, Expression> values = null, bool passInit = false) where T : BaseVM + //{ + // SetValuesParser p = new SetValuesParser(); + // var dir = p.Parse(values); + // return CreateVM(typeof(T), null, Ids.Cast().ToArray(), dir, passInit) as T; + //} + + ///// + ///// Create a ViewModel, and pass Session,cache,dc...etc to the viewmodel + ///// + ///// The type of the viewmodelThe type of the viewmodel + ///// If the viewmodel is a BatchVM, the BatchVM's Ids property will be assigned + ///// use Lambda to set viewmodel's properties,use && for multiply properties, for example Wtm.CreateVM(values: x=>x.Field1=='a' && x.Field2 == 'b'); will set viewmodel's Field1 to 'a' and Field2 to 'b' + ///// if true, the viewmodel will not call InitVM internally + ///// ViewModel + //public T Wtm.CreateVM(long[] Ids, Expression> values = null, bool passInit = false) where T : BaseVM + //{ + // SetValuesParser p = new SetValuesParser(); + // var dir = p.Parse(values); + // return CreateVM(typeof(T), null, Ids.Cast().ToArray(), dir, passInit) as T; + //} + ///// + ///// Create a ViewModel, and pass Session,cache,dc...etc to the viewmodel + ///// + ///// The type of the viewmodelThe type of the viewmodel + ///// If the viewmodel is a BatchVM, the BatchVM's Ids property will be assigned + ///// use Lambda to set viewmodel's properties,use && for multiply properties, for example Wtm.CreateVM(values: x=>x.Field1=='a' && x.Field2 == 'b'); will set viewmodel's Field1 to 'a' and Field2 to 'b' + ///// if true, the viewmodel will not call InitVM internally + ///// ViewModel + //public T Wtm.CreateVM(string[] Ids, Expression> values = null, bool passInit = false) where T : BaseVM + //{ + // SetValuesParser p = new SetValuesParser(); + // var dir = p.Parse(values); + // return CreateVM(typeof(T), null, Ids.Cast().ToArray(), dir, passInit) as T; + //} + + ///// + ///// Create a ViewModel, and pass Session,cache,dc...etc to the viewmodel + ///// + ///// the fullname of the viewmodel's type + ///// If the viewmodel is a BaseCRUDVM, the data having this id will be fetched + ///// If the viewmodel is a BatchVM, the BatchVM's Ids property will be assigned + ///// if true, the viewmodel will not call InitVM internally + ///// ViewModel + //public BaseVM CreateVM(string VmFullName, object Id = null, object[] Ids = null, bool passInit = false) + //{ + // return CreateVM(Type.GetType(VmFullName), Id, Ids, null, passInit); + //} + //#endregion + + #region ReInit model + private void SetReInit(ModelStateDictionary msd, BaseVM model) + { + var reinit = model.GetType().GetTypeInfo().GetCustomAttributes(typeof(ReInitAttribute), false).Cast().SingleOrDefault(); + + if (ModelState.IsValid) + { + if (reinit != null && (reinit.ReInitMode == ReInitModes.SUCCESSONLY || reinit.ReInitMode == ReInitModes.ALWAYS)) + { + model.DoReInit(); + } + } + else + { + if (reinit == null || (reinit.ReInitMode == ReInitModes.FAILEDONLY || reinit.ReInitMode == ReInitModes.ALWAYS)) + { + model.DoReInit(); + } + } + } + #endregion + + #region Validate model + [NonAction] + public Dictionary RedoValidation(object item) + { + Dictionary rv = new Dictionary(); + TryValidateModel(item); + + foreach (var e in ControllerContext.ModelState) + { + if (e.Value.ValidationState == ModelValidationState.Invalid) + { + rv.Add(e.Key, e.Value.Errors.Select(x => x.ErrorMessage).ToSepratedString()); + } + } + + return rv; + } + #endregion + + #region update viewmodel + /// + /// Set viewmodel's properties to the matching items posted by user + /// + /// ViewModel + /// prefix + /// true if success + [NonAction] + public bool RedoUpdateModel(object vm, string prefix = null) + { + try + { + BaseVM bvm = vm as BaseVM; + foreach (var item in bvm.FC.Keys) + { + PropertyHelper.SetPropertyValue(vm, item, bvm.FC[item], prefix, true); + } + return true; + } + catch + { + return false; + } + } + #endregion + + [NonAction] + public FResult FFResult() + { + var rv = new FResult + { + Controller = this + }; + try + { + rv.Controller.Response?.Headers?.Add("IsScript", "true"); + } + catch { } + return rv; + } + + protected JsonResult JsonMore(object data, int statusCode = StatusCodes.Status200OK, string msg = "success") + { + return new JsonResult(new JsonResultT { Msg = msg, Code = statusCode, Data = data }); + } + + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Binders/DateRangeBinder.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Binders/DateRangeBinder.cs new file mode 100644 index 0000000..028373e --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Binders/DateRangeBinder.cs @@ -0,0 +1,56 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using WalkingTec.Mvvm.Core; + +namespace WalkingTec.Mvvm.Mvc.Binders +{ + /// + /// DateRangeBinder + /// DataRange model binding + /// + public class DateRangeBinder : IModelBinder + { + /// + /// BindModelAsync + /// + /// + /// + public Task BindModelAsync(ModelBindingContext bindingContext) + { + if (bindingContext == null) + { + throw new ArgumentNullException(nameof(bindingContext)); + } + var modelName = bindingContext.ModelName; + if (string.IsNullOrEmpty(modelName)) + { + bindingContext.Result = ModelBindingResult.Success(null); + return Task.CompletedTask; + } + + var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName); + if (valueProviderResult == ValueProviderResult.None) + { + modelName += "[Value]"; + valueProviderResult = bindingContext.ValueProvider.GetValue(modelName); + } + + if (valueProviderResult == ValueProviderResult.None) + return Task.CompletedTask; + + bindingContext.ModelState.SetModelValue(modelName, valueProviderResult); + + var value = valueProviderResult.FirstValue; + + if (string.IsNullOrEmpty(value)) + { + return Task.CompletedTask; + } + + if(DateRange.TryParse(value,out var dateRange)) + bindingContext.Result = ModelBindingResult.Success(dateRange); + return Task.CompletedTask; + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Binders/EnumBinder.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Binders/EnumBinder.cs new file mode 100644 index 0000000..807b189 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Binders/EnumBinder.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace WalkingTec.Mvvm.Mvc.Binders +{ + /// + /// EnumBinder + /// 忽略enum转换空字符串时的错误 + /// + public class EnumBinder : IModelBinder + { + /// + /// BindModelAsync + /// + /// + /// + public Task BindModelAsync(ModelBindingContext bindingContext) + { + if (bindingContext == null) + { + throw new ArgumentNullException(nameof(bindingContext)); + } + var modelName = bindingContext.ModelName; + if (string.IsNullOrEmpty(modelName)) + { + bindingContext.Result = ModelBindingResult.Success(null); + return Task.CompletedTask; + } + + var valueProviderResult = + bindingContext.ValueProvider.GetValue(modelName); + + if (valueProviderResult == ValueProviderResult.None) + { + return Task.CompletedTask; + } + + bindingContext.ModelState.SetModelValue(modelName, valueProviderResult); + + var value = valueProviderResult.FirstValue; + if (string.IsNullOrEmpty(value)) + { + return Task.CompletedTask; + } + else + { + bindingContext.Result = ModelBindingResult.Success(Enum.Parse(bindingContext.ModelType,value)); + } + return Task.CompletedTask; + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Binders/StringBinderProvider.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Binders/StringBinderProvider.cs new file mode 100644 index 0000000..46a6298 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Binders/StringBinderProvider.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; +using System; +using System.Collections; +using WalkingTec.Mvvm.Core; + +namespace WalkingTec.Mvvm.Mvc.Binders +{ + /// + /// CustomBinderProvider + /// + public class StringBinderProvider : IModelBinderProvider + { + /// + /// GetBinder + /// + /// + /// + public IModelBinder GetBinder(ModelBinderProviderContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Metadata.ModelType == typeof(DateRange)) + return new BinderTypeModelBinder(typeof(DateRangeBinder)); + + if (context.Metadata.ModelType == typeof(string)) + { + return new BinderTypeModelBinder(typeof(StringIgnoreLTGTBinder)); + } + return null; + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Binders/StringIgnoreLTGTBinder.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Binders/StringIgnoreLTGTBinder.cs new file mode 100644 index 0000000..17e99e2 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Binders/StringIgnoreLTGTBinder.cs @@ -0,0 +1,61 @@ +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using System; +using System.Linq; +using System.Threading.Tasks; +using System.Web; + +namespace WalkingTec.Mvvm.Mvc.Binders +{ + /// + /// StringIgnoreLTGTBinder + /// 忽略客户端提交的 <及>字符 + /// + public class StringIgnoreLTGTBinder : IModelBinder + { + /// + /// BindModelAsync + /// + /// + /// + public Task BindModelAsync(ModelBindingContext bindingContext) + { + if (bindingContext == null) + { + throw new ArgumentNullException(nameof(bindingContext)); + } + var modelName = bindingContext.ModelName; + if (string.IsNullOrEmpty(modelName)) + { + bindingContext.Result = ModelBindingResult.Success(null); + return Task.CompletedTask; + } + + var valueProviderResult = + bindingContext.ValueProvider.GetValue(modelName); + + if (valueProviderResult == ValueProviderResult.None) + { + return Task.CompletedTask; + } + + bindingContext.ModelState.SetModelValue(modelName, valueProviderResult); + + var value = valueProviderResult.FirstValue; + + var actDescriptor = bindingContext.ActionContext.ActionDescriptor as ControllerActionDescriptor; + var count = actDescriptor + .MethodInfo + .CustomAttributes + .Where(x => x.AttributeType == typeof(StringNeedLTGTAttribute)).Count(); + + if (count == 0) + { + value = HttpUtility.HtmlEncode(value); + } + + bindingContext.Result = ModelBindingResult.Success(value); + return Task.CompletedTask; + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Binders/StringNeedLTGTAttribute.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Binders/StringNeedLTGTAttribute.cs new file mode 100644 index 0000000..2f2174d --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Binders/StringNeedLTGTAttribute.cs @@ -0,0 +1,12 @@ +using System; + +namespace WalkingTec.Mvvm.Mvc.Binders +{ + /// + /// StringNeedLTGT + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class StringNeedLTGTAttribute : Attribute + { + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/CodeGenListVM.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/CodeGenListVM.cs new file mode 100644 index 0000000..1f44ced --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/CodeGenListVM.cs @@ -0,0 +1,272 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using WalkingTec.Mvvm.Core; +using WalkingTec.Mvvm.Core.Attributes; +using WalkingTec.Mvvm.Core.Extensions; + +namespace WalkingTec.Mvvm.Mvc +{ + public class CodeGenListVM : BasePagedListVM + { + + public string ModelFullName { get; set; } + + public CodeGenListVM() + { + NeedPage = false; + } + + protected override IEnumerable> InitGridHeader() + { + return new List> + { + this.MakeGridHeader(x=>x.FieldName,200).SetFormat((entity,val)=>{return withHidden($"FieldInfos[{entity.Index}].FieldName",entity.FieldName); }), + this.MakeGridHeader(x=>x.FieldDes,200), + this.MakeGridHeader(x=>x.SubField,200).SetFormat((entity,val)=>{return subField($"FieldInfos[{entity.Index}]",entity); }), + this.MakeGridHeader(x=>x.IsSearcherField,150).SetFormat((entity,val)=>{return getCheckBox($"FieldInfos[{entity.Index}].IsSearcherField",entity.IsSearcherField); }), + this.MakeGridHeader(x=>x.IsListField,150).SetFormat((entity,val)=>{return getCheckBox($"FieldInfos[{entity.Index}].IsListField",entity.IsListField); }), + this.MakeGridHeader(x=>x.IsFormField,150).SetFormat((entity,val)=>{return getCheckBox($"FieldInfos[{entity.Index}].IsFormField",entity.IsFormField); }), + this.MakeGridHeader(x=>x.IsImportField,150).SetFormat((entity,val)=>{return getCheckBox($"FieldInfos[{entity.Index}].IsImportField",entity.IsImportField); }), + this.MakeGridHeader(x=>x.IsBatchField,150).SetFormat((entity,val)=>{return getCheckBox($"FieldInfos[{entity.Index}].IsBatchField",entity.IsBatchField); }) + }; + } + + private string getCheckBox(string fieldname, bool val) + { + return UIService.MakeCheckBox(val, name: fieldname, value:"true"); + } + + private string withHidden(string fieldname, string val) + { + return val + $""; + } + + private string subField(string fieldname, CodeGenListView entity) + { + string rv = $""; + rv += $""; + if (string.IsNullOrEmpty(entity.LinkedType) == false) + { + var linktype = Type.GetType(entity.LinkedType); + if (linktype != typeof(FileAttachment)) + { + var subpros = Type.GetType(entity.LinkedType).GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance).Where(x=>x.GetMemberType() == typeof(string) && x.Name != "BatchError").OrderBy(x => x.Name).ToList().ToListItems(x => x.Name, x => x.Name); + var subproswithname = subpros.Where(x => x.Text.ToLower().Contains("name")).ToList(); + var subproswithoutname = subpros.Where(x => x.Text.ToLower().Contains("name") == false).ToList(); + subpros = new List(); + subpros.AddRange(subproswithname); + subpros.AddRange(subproswithoutname); + if(subpros.Count == 0) + { + subpros.Add(new ComboSelectListItem { Text = "Id", Value = "Id" }); + } + rv += UIService.MakeCombo(fieldname + ".SubField", subpros); + } + else + { + rv += $""; + } + } + return rv; + } + + public override IOrderedQueryable GetSearchQuery() + { + Type modeltype = Type.GetType(ModelFullName); + var pros = modeltype.GetAllProperties(); + List lv = new List(); + int count = 0; + List skipFields = new List() + { + nameof(TopBasePoco.BatchError), + nameof(TopBasePoco.Checked), + nameof(TopBasePoco.ExcelIndex), + }; + if (typeof(IBasePoco).IsAssignableFrom(modeltype)) + { + skipFields.AddRange( + new string[]{ + nameof(IBasePoco.CreateBy), + nameof(IBasePoco.CreateTime), + nameof(IBasePoco.UpdateBy), + nameof(IBasePoco.UpdateTime) } + ); + } + if (typeof(IPersistPoco).IsAssignableFrom(modeltype)) + { + skipFields.Add(nameof(IPersistPoco.IsValid)); + } + + List ignoreField = new List(); + foreach (var pro in pros) + { + if (skipFields.Contains(pro.Name) == false) + { + if(pro.CanWrite == false) + { + continue; + } + if(pro.Name.ToLower() == "id" && pro.PropertyType != typeof(string)) + { + continue; + } + CodeGenListView view = new CodeGenListView() + { + FieldName = pro.Name, + FieldDes = pro.GetPropertyDisplayName(), + SubIdField = "", + Index = count + }; + var notmapped = pro.GetCustomAttributes(typeof(NotMappedAttribute), false).FirstOrDefault(); + Type checktype = pro.PropertyType; + if (pro.PropertyType.IsNullable()) + { + checktype = pro.PropertyType.GetGenericArguments()[0]; + } + if (ignoreField.Contains(checktype.Name)) + { + continue; + } + bool show = false; + view.IsFormField = true; + view.IsListField = true; + view.IsImportField = true; + if (checktype.IsPrimitive || checktype == typeof(string) || checktype == typeof(DateTime) || checktype.IsEnum() || checktype == typeof(decimal)) + { + show = true; + } + if (typeof(TopBasePoco).IsAssignableFrom(checktype)){ + var fk = DC.GetFKName2(modeltype, pro.Name); + if(fk != null) + { + ignoreField.Add(fk); + show = true; + } + if (checktype == typeof(FileAttachment)) + { + view.IsImportField = false; + view.FieldDes += $"({MvcProgram._localizer["Codegen.Attachment"]})"; + } + else + { + view.FieldDes += $"({MvcProgram._localizer["Codegen.OneToMany"]})"; + } + view.LinkedType = checktype.AssemblyQualifiedName; + if (fk != null) + { + if (modeltype.GetSingleProperty(fk) == null) + { + view.FieldDes = $"(Error:Can't find {fk.Replace("ID","Id")} in {checktype.Name})"; + } + } + } + if (checktype.IsList()) + { + checktype = pro.PropertyType.GetGenericArguments()[0]; + if (checktype.IsNullable()) + { + checktype = checktype.GetGenericArguments()[0]; + } + var middletable = checktype.GetCustomAttributes(typeof(MiddleTableAttribute), false).FirstOrDefault(); + if (middletable != null) + { + view.FieldDes += $"({MvcProgram._localizer["Codegen.ManyToMany"]})"; + view.IsImportField = false; + var subpros = checktype.GetAllProperties(); + foreach (var spro in subpros) + { + if(skipFields.Contains(spro.Name) == false) + { + Type subchecktype = spro.PropertyType; + if (spro.PropertyType.IsNullable()) + { + subchecktype = spro.PropertyType.GetGenericArguments()[0]; + } + if (typeof(TopBasePoco).IsAssignableFrom(subchecktype) && subchecktype != modeltype) + { + view.LinkedType = subchecktype.AssemblyQualifiedName; + var fk = DC.GetFKName2(checktype, spro.Name); + view.SubIdField = fk; + show = true; + if(checktype.GetSingleProperty(fk) == null) { + view.FieldDes = $"(Error:Can't find {fk.Replace("ID", "Id")} in {checktype.Name})"; + } + } + } + } + } + } + if (notmapped != null) + { + view.FieldDes += "(NotMapped)"; + view.IsFormField = false; + view.IsSearcherField = false; + view.IsBatchField = false; + view.IsImportField = false; + view.IsListField = false; + } + if (show == true) + { + lv.Add(view); + count++; + } + } + } + + for (int i = 0; i < lv.Count; i++) + { + if (ignoreField.Contains(lv[i].FieldName)) + { + for(int j = i; j < lv.Count; j++) + { + lv[j].Index--; + } + lv.RemoveAt(i); + i--; + } + } + + return lv.AsQueryable().OrderBy(x => x.FieldName); + } + } + + public class CodeGenListView : BasePoco + { + [Display(Name = "_Admin.FieldName")] + public string FieldName { get; set; } + + [Display(Name = "Codegen.FieldDes")] + public string FieldDes { get; set; } + + + [Display(Name = "Codegen.IsSearcherField")] + public bool IsSearcherField { get; set; } + + [Display(Name = "Codegen.IsListField")] + public bool IsListField { get; set; } + + [Display(Name = "Codegen.IsFormField")] + public bool IsFormField { get; set; } + + + [Display(Name = "Codegen.SubField")] + public string SubField { get; set; } + + public string SubIdField { get; set; } + + [Display(Name = "Codegen.IsImportField")] + public bool IsImportField { get; set; } + + [Display(Name = "Codegen.IsBatchField")] + public bool IsBatchField { get; set; } + + public int Index { get; set; } + + [Display(Name = "Codegen.LinkedType")] + public string LinkedType { get; set; } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/CodeGenVM.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/CodeGenVM.cs new file mode 100644 index 0000000..810ac9a --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/CodeGenVM.cs @@ -0,0 +1,2737 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using WalkingTec.Mvvm.Core; +using WalkingTec.Mvvm.Core.Extensions; + +namespace WalkingTec.Mvvm.Mvc +{ + public enum ApiAuthMode + { + [Display(Name = "Both Jwt and Cookie")] + Both, + [Display(Name = "Jwt")] + Jwt, + [Display(Name = "Cookie")] + Cookie + } + + [ReInit(ReInitModes.ALWAYS)] + public class CodeGenVM : BaseVM + { + public CodeGenListVM FieldList { get; set; } + + public List FieldInfos { get; set; } + + public string PreviewFile { get; set; } + + public UIEnum UI { get; set; } + + [Display(Name = "Codegen.GenApi")] + public bool IsApi { get; set; } + + [Display(Name = "Codegen.AuthMode")] + public ApiAuthMode AuthMode { get; set; } + + public string ModelName + { + get + { + return SelectedModel?.Split(',').FirstOrDefault()?.Split('.').LastOrDefault() ?? ""; + } + } + [Display(Name = "Codegen.ModelNS")] + [ValidateNever()] + public string ModelNS => SelectedModel?.Split(',').FirstOrDefault()?.Split('.').SkipLast(1).ToSepratedString(seperator: "."); + [Display(Name = "Codegen.ModuleName")] + [Required(ErrorMessage = "Validate.{0}required")] + public string ModuleName { get; set; } + [RegularExpression("^[A-Za-z_]+", ErrorMessage = "Codegen.EnglishOnly")] + public string Area { get; set; } + [ValidateNever()] + [BindNever()] + public List AllModels { get; set; } + [Required(ErrorMessage = "Validate.{0}required")] + [Display(Name = "_Admin.SelectedModel")] + public string SelectedModel { get; set; } + [ValidateNever()] + public string EntryDir { get; set; } + + + public string _mainDir; + [ValidateNever()] + public string MainDir + { + get + { + if (_mainDir == null) + { + int? index = EntryDir?.IndexOf($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}Debug{Path.DirectorySeparatorChar}"); + if (index == null || index < 0) + { + index = EntryDir?.IndexOf($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}Release{Path.DirectorySeparatorChar}") ?? 0; + } + + _mainDir = EntryDir?.Substring(0, index.Value); + } + return _mainDir; + } + set + { + _mainDir = value; + } + } + + public string _vmdir; + [ValidateNever()] + public string VmDir + { + get + { + if (_vmdir == null) + { + var up = Directory.GetParent(MainDir); + var vmdir = up.GetDirectories().Where(x => x.Name.ToLower().EndsWith(".viewmodel")).FirstOrDefault(); + if (vmdir == null) + { + if (string.IsNullOrEmpty(Area)) + { + vmdir = Directory.CreateDirectory(MainDir + $"{Path.DirectorySeparatorChar}ViewModels{Path.DirectorySeparatorChar}{ModelName}VMs"); + } + else + { + vmdir = Directory.CreateDirectory(MainDir + $"{Path.DirectorySeparatorChar}Areas{Path.DirectorySeparatorChar}{Area}{Path.DirectorySeparatorChar}ViewModels{Path.DirectorySeparatorChar}{ModelName}VMs"); + } + } + else + { + if (string.IsNullOrEmpty(Area)) + { + vmdir = Directory.CreateDirectory(vmdir.FullName + $"{Path.DirectorySeparatorChar}{ModelName}VMs"); + } + else + { + vmdir = Directory.CreateDirectory(vmdir.FullName + $"{Path.DirectorySeparatorChar}{Area}{Path.DirectorySeparatorChar}{ModelName}VMs"); + } + + } + _vmdir = vmdir.FullName; + } + return _vmdir; + } + } + + public string _sharedir; + [ValidateNever()] + public string ShareDir + { + get + { + if (_sharedir == null) + { + var up = Directory.GetParent(MainDir); + var sharedir = up.GetDirectories().Where(x => x.Name.ToLower().EndsWith(".shared")).FirstOrDefault(); + if (string.IsNullOrEmpty(Area)) + { + sharedir = Directory.CreateDirectory(sharedir.FullName + $"{Path.DirectorySeparatorChar}Pages{Path.DirectorySeparatorChar}{ModelName}"); + } + else + { + sharedir = Directory.CreateDirectory(sharedir.FullName + $"{Path.DirectorySeparatorChar}Pages{Path.DirectorySeparatorChar}{Area}{Path.DirectorySeparatorChar}{ModelName}"); + } + + _sharedir = sharedir.FullName; + } + return _sharedir; + } + } + + + public string _testdir; + [ValidateNever()] + public string TestDir + { + get + { + if (_testdir == null) + { + var up = Directory.GetParent(MainDir); + var testdir = up.GetDirectories().Where(x => x.Name.ToLower().EndsWith(".test")).FirstOrDefault(); + _testdir = testdir?.FullName; + } + return _testdir; + } + } + + + public string _controllerdir; + [ValidateNever()] + public string ControllerDir + { + get + { + if (_controllerdir == null) + { + if (string.IsNullOrEmpty(Area)) + { + _controllerdir = Directory.CreateDirectory(MainDir + $"{Path.DirectorySeparatorChar}Controllers").FullName; + } + else + { + _controllerdir = Directory.CreateDirectory(MainDir + $"{Path.DirectorySeparatorChar}Areas{Path.DirectorySeparatorChar}{Area}{Path.DirectorySeparatorChar}Controllers").FullName; + } + } + return _controllerdir; + } + } + + public string _viewdir; + [ValidateNever()] + public string ViewDir + { + get + { + if (_viewdir == null) + { + if (string.IsNullOrEmpty(Area)) + { + _viewdir = Directory.CreateDirectory(MainDir + $"{Path.DirectorySeparatorChar}Views{Path.DirectorySeparatorChar}{ModelName}").FullName; + } + else + { + _viewdir = Directory.CreateDirectory(MainDir + $"{Path.DirectorySeparatorChar}Areas{Path.DirectorySeparatorChar}{Area}{Path.DirectorySeparatorChar}Views{Path.DirectorySeparatorChar}{ModelName}").FullName; + } + } + return _viewdir; + } + } + + + private string _mainNs; + public string MainNS + { + get + { + int index = MainDir.LastIndexOf(Path.DirectorySeparatorChar); + if (index > 0) + { + _mainNs = MainDir[(index + 1)..]; + } + else + { + _mainNs = MainDir; + } + return _mainNs; + } + set + { + _mainNs = value; + } + + } + + private string _controllerNs; + [Display(Name = "Codegen.ControllerNs")] + [ValidateNever()] + public string ControllerNs + { + get + { + if (_controllerNs == null) + { + _controllerNs = MainNS + ".Controllers"; + } + return _controllerNs; + } + set + { + _controllerNs = value; + } + } + + private string _testNs; + [Display(Name = "Codegen.TestNs")] + [ValidateNever()] + public string TestNs + { + get + { + if (_testNs == null) + { + _testNs = MainNS + ".Test"; + } + return _testNs; + } + set + { + _testNs = value; + } + } + + private string _dataNs; + [Display(Name = "Codegen.DataNs")] + [ValidateNever()] + public string DataNs + { + get + { + if (_dataNs == null) + { + var up = Directory.GetParent(MainDir); + var vmdir = up.GetDirectories().Where(x => x.Name.ToLower().EndsWith(".dataaccess")).FirstOrDefault(); + if (vmdir == null) + { + _dataNs = MainNS; + } + else + { + _dataNs = MainNS + ".DataAccess"; ; + } + } + return _dataNs; + } + set + { + _dataNs = value; + } + } + + + private string _vmNs; + [Display(Name = "Codegen.VMNs")] + [ValidateNever()] + public string VMNs + { + get + { + if (_vmNs == null) + { + var up = Directory.GetParent(MainDir); + var vmdir = up.GetDirectories().Where(x => x.Name.ToLower().EndsWith(".viewmodel")).FirstOrDefault(); + if (vmdir == null) + { + if (string.IsNullOrEmpty(Area)) + { + _vmNs = MainNS + $".ViewModels.{ModelName}VMs"; + } + else + { + _vmNs = MainNS + $".{Area}.ViewModels.{ModelName}VMs"; + } + } + else + { + int index = vmdir.FullName.LastIndexOf(Path.DirectorySeparatorChar); + if (index > 0) + { + _vmNs = vmdir.FullName[(index + 1)..]; + } + else + { + _vmNs = vmdir.FullName; + } + if (string.IsNullOrEmpty(Area)) + { + _vmNs += $".{ModelName}VMs"; + } + else + { + _vmNs += $".{Area}.{ModelName}VMs"; + } + } + } + return _vmNs; + } + set + { + _vmNs = value; + } + } + + protected override void InitVM() + { + if (string.IsNullOrEmpty(SelectedModel) == false) + { + foreach (var item in ConfigInfo.Connections) + { + var dc = item.CreateDC(); + Type t = typeof(DbSet<>).MakeGenericType(Type.GetType(SelectedModel)); + var exist = dc.GetType().GetSingleProperty(x => x.PropertyType == t); + if (exist != null) + { + this.DC = dc; + } + } + + } + + FieldList = new CodeGenListVM(); + FieldList.CopyContext(this); + } + public void DoGen() + { + File.WriteAllText($"{ControllerDir}{Path.DirectorySeparatorChar}{ModelName}{(IsApi == true ? "Api" : "")}Controller.cs", GenerateController(), Encoding.UTF8); + + File.WriteAllText($"{VmDir}{Path.DirectorySeparatorChar}{ModelName}{(IsApi == true ? "Api" : "")}VM.cs", GenerateVM("CrudVM"), Encoding.UTF8); + File.WriteAllText($"{VmDir}{Path.DirectorySeparatorChar}{ModelName}{(IsApi == true ? "Api" : "")}ListVM.cs", GenerateVM("ListVM"), Encoding.UTF8); + File.WriteAllText($"{VmDir}{Path.DirectorySeparatorChar}{ModelName}{(IsApi == true ? "Api" : "")}BatchVM.cs", GenerateVM("BatchVM"), Encoding.UTF8); + File.WriteAllText($"{VmDir}{Path.DirectorySeparatorChar}{ModelName}{(IsApi == true ? "Api" : "")}ImportVM.cs", GenerateVM("ImportVM"), Encoding.UTF8); + File.WriteAllText($"{VmDir}{Path.DirectorySeparatorChar}{ModelName}{(IsApi == true ? "Api" : "")}Searcher.cs", GenerateVM("Searcher"), Encoding.UTF8); + + if (IsApi == false) + { + if (UI == UIEnum.LayUI) + { + File.WriteAllText($"{ViewDir}{Path.DirectorySeparatorChar}Index.cshtml", GenerateView("ListView"), Encoding.UTF8); + File.WriteAllText($"{ViewDir}{Path.DirectorySeparatorChar}Create.cshtml", GenerateView("CreateView"), Encoding.UTF8); + File.WriteAllText($"{ViewDir}{Path.DirectorySeparatorChar}Edit.cshtml", GenerateView("EditView"), Encoding.UTF8); + File.WriteAllText($"{ViewDir}{Path.DirectorySeparatorChar}Delete.cshtml", GenerateView("DeleteView"), Encoding.UTF8); + File.WriteAllText($"{ViewDir}{Path.DirectorySeparatorChar}Details.cshtml", GenerateView("DetailsView"), Encoding.UTF8); + File.WriteAllText($"{ViewDir}{Path.DirectorySeparatorChar}Import.cshtml", GenerateView("ImportView"), Encoding.UTF8); + File.WriteAllText($"{ViewDir}{Path.DirectorySeparatorChar}BatchEdit.cshtml", GenerateView("BatchEditView"), Encoding.UTF8); + File.WriteAllText($"{ViewDir}{Path.DirectorySeparatorChar}BatchDelete.cshtml", GenerateView("BatchDeleteView"), Encoding.UTF8); + } + if (UI == UIEnum.React || UI == UIEnum.VUE) + { + if (Directory.Exists($"{MainDir}{Path.DirectorySeparatorChar}ClientApp{Path.DirectorySeparatorChar}src{Path.DirectorySeparatorChar}pages{Path.DirectorySeparatorChar}{ModelName.ToLower()}") == false) + { + Directory.CreateDirectory($"{MainDir}{Path.DirectorySeparatorChar}ClientApp{Path.DirectorySeparatorChar}src{Path.DirectorySeparatorChar}pages{Path.DirectorySeparatorChar}{ModelName.ToLower()}"); + } + if (Directory.Exists($"{MainDir}{Path.DirectorySeparatorChar}ClientApp{Path.DirectorySeparatorChar}src{Path.DirectorySeparatorChar}pages{Path.DirectorySeparatorChar}{ModelName.ToLower()}{Path.DirectorySeparatorChar}views") == false) + { + Directory.CreateDirectory($"{MainDir}{Path.DirectorySeparatorChar}ClientApp{Path.DirectorySeparatorChar}src{Path.DirectorySeparatorChar}pages{Path.DirectorySeparatorChar}{ModelName.ToLower()}{Path.DirectorySeparatorChar}views"); + } + if (Directory.Exists($"{MainDir}{Path.DirectorySeparatorChar}ClientApp{Path.DirectorySeparatorChar}src{Path.DirectorySeparatorChar}pages{Path.DirectorySeparatorChar}{ModelName.ToLower()}{Path.DirectorySeparatorChar}store") == false) + { + Directory.CreateDirectory($"{MainDir}{Path.DirectorySeparatorChar}ClientApp{Path.DirectorySeparatorChar}src{Path.DirectorySeparatorChar}pages{Path.DirectorySeparatorChar}{ModelName.ToLower()}{Path.DirectorySeparatorChar}store"); + } + if (UI == UIEnum.React) + { + File.WriteAllText($"{MainDir}{Path.DirectorySeparatorChar}ClientApp{Path.DirectorySeparatorChar}src{Path.DirectorySeparatorChar}pages{Path.DirectorySeparatorChar}{ModelName.ToLower()}{Path.DirectorySeparatorChar}views{Path.DirectorySeparatorChar}action.tsx", GenerateReactView("action"), Encoding.UTF8); + File.WriteAllText($"{MainDir}{Path.DirectorySeparatorChar}ClientApp{Path.DirectorySeparatorChar}src{Path.DirectorySeparatorChar}pages{Path.DirectorySeparatorChar}{ModelName.ToLower()}{Path.DirectorySeparatorChar}views{Path.DirectorySeparatorChar}forms.tsx", GenerateReactView("forms"), Encoding.UTF8); + File.WriteAllText($"{MainDir}{Path.DirectorySeparatorChar}ClientApp{Path.DirectorySeparatorChar}src{Path.DirectorySeparatorChar}pages{Path.DirectorySeparatorChar}{ModelName.ToLower()}{Path.DirectorySeparatorChar}views{Path.DirectorySeparatorChar}models.tsx", GenerateReactView("models"), Encoding.UTF8); + File.WriteAllText($"{MainDir}{Path.DirectorySeparatorChar}ClientApp{Path.DirectorySeparatorChar}src{Path.DirectorySeparatorChar}pages{Path.DirectorySeparatorChar}{ModelName.ToLower()}{Path.DirectorySeparatorChar}views{Path.DirectorySeparatorChar}other.tsx", GenerateReactView("other"), Encoding.UTF8); + File.WriteAllText($"{MainDir}{Path.DirectorySeparatorChar}ClientApp{Path.DirectorySeparatorChar}src{Path.DirectorySeparatorChar}pages{Path.DirectorySeparatorChar}{ModelName.ToLower()}{Path.DirectorySeparatorChar}views{Path.DirectorySeparatorChar}search.tsx", GenerateReactView("search"), Encoding.UTF8); + File.WriteAllText($"{MainDir}{Path.DirectorySeparatorChar}ClientApp{Path.DirectorySeparatorChar}src{Path.DirectorySeparatorChar}pages{Path.DirectorySeparatorChar}{ModelName.ToLower()}{Path.DirectorySeparatorChar}views{Path.DirectorySeparatorChar}table.tsx", GenerateReactView("table"), Encoding.UTF8); + File.WriteAllText($"{MainDir}{Path.DirectorySeparatorChar}ClientApp{Path.DirectorySeparatorChar}src{Path.DirectorySeparatorChar}pages{Path.DirectorySeparatorChar}{ModelName.ToLower()}{Path.DirectorySeparatorChar}store{Path.DirectorySeparatorChar}index.ts", GetResource("index.txt", "Spa.React.store").Replace("$modelname$", ModelName.ToLower()), Encoding.UTF8); + File.WriteAllText($"{MainDir}{Path.DirectorySeparatorChar}ClientApp{Path.DirectorySeparatorChar}src{Path.DirectorySeparatorChar}pages{Path.DirectorySeparatorChar}{ModelName.ToLower()}{Path.DirectorySeparatorChar}index.tsx", GetResource("index.txt", "Spa.React").Replace("$modelname$", ModelName.ToLower()), Encoding.UTF8); + File.WriteAllText($"{MainDir}{Path.DirectorySeparatorChar}ClientApp{Path.DirectorySeparatorChar}src{Path.DirectorySeparatorChar}pages{Path.DirectorySeparatorChar}{ModelName.ToLower()}{Path.DirectorySeparatorChar}style.less", GetResource("style.txt", "Spa.React").Replace("$modelname$", ModelName.ToLower()), Encoding.UTF8); + } + if (UI == UIEnum.VUE) + { + List apipneeded = new List(); + File.WriteAllText($"{MainDir}{Path.DirectorySeparatorChar}ClientApp{Path.DirectorySeparatorChar}src{Path.DirectorySeparatorChar}pages{Path.DirectorySeparatorChar}{ModelName.ToLower()}{Path.DirectorySeparatorChar}index.vue", GenerateVUEView("index", apipneeded), Encoding.UTF8); + File.WriteAllText($"{MainDir}{Path.DirectorySeparatorChar}ClientApp{Path.DirectorySeparatorChar}src{Path.DirectorySeparatorChar}pages{Path.DirectorySeparatorChar}{ModelName.ToLower()}{Path.DirectorySeparatorChar}config.ts", GenerateVUEView("config", apipneeded), Encoding.UTF8); + File.WriteAllText($"{MainDir}{Path.DirectorySeparatorChar}ClientApp{Path.DirectorySeparatorChar}src{Path.DirectorySeparatorChar}pages{Path.DirectorySeparatorChar}{ModelName.ToLower()}{Path.DirectorySeparatorChar}views{Path.DirectorySeparatorChar}dialog-form.vue", GenerateVUEView("views.dialog-form", apipneeded), Encoding.UTF8); + File.WriteAllText($"{MainDir}{Path.DirectorySeparatorChar}ClientApp{Path.DirectorySeparatorChar}src{Path.DirectorySeparatorChar}pages{Path.DirectorySeparatorChar}{ModelName.ToLower()}{Path.DirectorySeparatorChar}store{Path.DirectorySeparatorChar}index.ts", GetResource("index.txt", "Spa.Vue.store").Replace("$modelname$", ModelName.ToLower()), Encoding.UTF8); + File.WriteAllText($"{MainDir}{Path.DirectorySeparatorChar}ClientApp{Path.DirectorySeparatorChar}src{Path.DirectorySeparatorChar}pages{Path.DirectorySeparatorChar}{ModelName.ToLower()}{Path.DirectorySeparatorChar}store{Path.DirectorySeparatorChar}api.ts", GenerateVUEView("store.api", apipneeded), Encoding.UTF8); + } + var index = File.ReadAllText($"{MainDir}{Path.DirectorySeparatorChar}ClientApp{Path.DirectorySeparatorChar}src{Path.DirectorySeparatorChar}pages{Path.DirectorySeparatorChar}index.ts"); + if (index.Contains($"path: '/{ModelName.ToLower()}'") == false) + { + if (UI == UIEnum.React) + { + index = index.Replace("/**WTM**/", $@" +, {ModelName.ToLower()}: {{ + name: '{ModuleName.ToLower()}', + path: '/{ModelName.ToLower()}', + controller: '{ControllerNs},{ModelName}', + component: React.lazy(() => import('./{ModelName.ToLower()}')) + }} +/**WTM**/ + "); + } + if (UI == UIEnum.VUE) + { + index = index.Replace("/**WTM**/", $@" +, {ModelName.ToLower()}: {{ + name: '{ModuleName.ToLower()}', + path: '/{ModelName.ToLower()}', + controller: '{ControllerNs},{ModelName}' + }} +/**WTM**/ + "); + + } + File.WriteAllText($"{MainDir}{Path.DirectorySeparatorChar}ClientApp{Path.DirectorySeparatorChar}src{Path.DirectorySeparatorChar}pages{Path.DirectorySeparatorChar}index.ts", index, Encoding.UTF8); + } + string menu = ""; + if (UI == UIEnum.React) + { + menu = File.ReadAllText($"{MainDir}{Path.DirectorySeparatorChar}ClientApp{Path.DirectorySeparatorChar}public{Path.DirectorySeparatorChar}subMenu.json"); + if (menu.Contains($@"""Url"": ""/{ModelName.ToLower()}""") == false) + { + var i = menu.LastIndexOf("}"); + menu = menu.Insert(i + 1, $@" +,{{ + ""Id"": ""{Guid.NewGuid()}"", + ""ParentId"": null, + ""Text"": ""{ModuleName.ToLower()}"", + ""Url"": ""/{ModelName.ToLower()}"" + }} +"); + File.WriteAllText($"{MainDir}{Path.DirectorySeparatorChar}ClientApp{Path.DirectorySeparatorChar}public{Path.DirectorySeparatorChar}subMenu.json", menu, Encoding.UTF8); + + } + } + if (UI == UIEnum.VUE) + { + menu = File.ReadAllText($"{MainDir}{Path.DirectorySeparatorChar}ClientApp{Path.DirectorySeparatorChar}src{Path.DirectorySeparatorChar}subMenu.json"); + if (menu.Contains($@"""Url"": ""/{ModelName.ToLower()}""") == false) + { + var i = menu.LastIndexOf("}"); + menu = menu.Insert(i + 1, $@" +,{{ + ""Id"": ""{Guid.NewGuid()}"", + ""ParentId"": null, + ""Text"": ""{ModuleName.ToLower()}"", + ""Url"": ""/{ModelName.ToLower()}"" + }} +"); + File.WriteAllText($"{MainDir}{Path.DirectorySeparatorChar}ClientApp{Path.DirectorySeparatorChar}src{Path.DirectorySeparatorChar}subMenu.json", menu, Encoding.UTF8); + + } + } + } + + if(UI == UIEnum.Blazor) + { + File.WriteAllText($"{ShareDir}{Path.DirectorySeparatorChar}Index.razor", GenerateBlazorView("Index"), Encoding.UTF8); + File.WriteAllText($"{ShareDir}{Path.DirectorySeparatorChar}Create.razor", GenerateBlazorView("Create"), Encoding.UTF8); + File.WriteAllText($"{ShareDir}{Path.DirectorySeparatorChar}Edit.razor", GenerateBlazorView("Edit"), Encoding.UTF8); + File.WriteAllText($"{ShareDir}{Path.DirectorySeparatorChar}Details.razor", GenerateBlazorView("Details"), Encoding.UTF8); + File.WriteAllText($"{ShareDir}{Path.DirectorySeparatorChar}Import.razor", GenerateBlazorView("Import"), Encoding.UTF8); + } + } + var test = GenerateTest(); + if (test != "") + { + if (UI == UIEnum.LayUI && IsApi == false) + { + File.WriteAllText($"{TestDir}{Path.DirectorySeparatorChar}{ModelName}ControllerTest.cs", test, Encoding.UTF8); + } + else + { + File.WriteAllText($"{TestDir}{Path.DirectorySeparatorChar}{ModelName}ApiTest.cs", test, Encoding.UTF8); + } + } + } + + public string GenerateController() + { + string dir = ""; + string jwt = ""; + if (UI == UIEnum.LayUI && IsApi == false) + { + dir = "Mvc"; + } + else + { + dir = "Spa"; + if(UI == UIEnum.Blazor) + { + dir = "Spa.Blazor"; + } + switch (AuthMode) + { + case ApiAuthMode.Both: + jwt = "[AuthorizeJwtWithCookie]"; + break; + case ApiAuthMode.Jwt: + jwt = "[AuthorizeJwt]"; + break; + case ApiAuthMode.Cookie: + jwt = "[AuthorizeCookie]"; + break; + default: + break; + } + } + var rv = GetResource("Controller.txt", dir).Replace("$jwt$", jwt).Replace("$vmnamespace$", VMNs).Replace("$namespace$", ControllerNs).Replace("$des$", ModuleName).Replace("$modelname$", ModelName).Replace("$modelnamespace$", ModelNS).Replace("$controllername$", $"{ModelName}{(IsApi == true ? "Api" : "")}"); + if (string.IsNullOrEmpty(Area)) + { + rv = rv.Replace("$area$", ""); + } + else + { + rv = rv.Replace("$area$", $"[Area(\"{Area}\")]"); + } + //生成api中获取下拉菜单数据的api + //如果一个一对多关联其他类的字段是搜索条件或者表单字段,则生成对应的获取关联表数据的api + if (UI != UIEnum.LayUI || IsApi == true) + { + StringBuilder other = new StringBuilder(); + List pros = FieldInfos.Where(x => x.IsSearcherField == true || x.IsFormField == true).ToList(); + List existSubPro = new List(); + for (int i = 0; i < pros.Count; i++) + { + var item = pros[i]; + if ((item.InfoType == FieldInfoType.One2Many || item.InfoType == FieldInfoType.Many2Many) && item.SubField != "`file") + { + var subtype = Type.GetType(item.RelatedField); + var subpro = subtype.GetSingleProperty(item.SubField); + var key = subtype.FullName + ":" + subpro.Name; + existSubPro.Add(key); + int count = existSubPro.Where(x => x == key).Count(); + if (count == 1) + { + + other.AppendLine($@" + [HttpGet(""Get{subtype.Name}s"")] + public ActionResult Get{subtype.Name}s() + {{ + return Ok(DC.Set<{subtype.Name}>().GetSelectListItems(Wtm, x => x.{item.SubField})); + }}"); + } + } + } + rv = rv.Replace("$other$", other.ToString()); + rv = GetRelatedNamespace(pros, rv); + } + return rv; + } + + public string GenerateVM(string name) + { + var rv = GetResource($"{name}.txt").Replace("$modelnamespace$", ModelNS).Replace("$vmnamespace$", VMNs).Replace("$modelname$", ModelName).Replace("$area$", $"{Area ?? ""}").Replace("$classname$", $"{ModelName}{(IsApi == true ? "Api" : "")}"); + if (name == "Searcher" || name == "BatchVM") + { + string prostring = ""; + string initstr = ""; + Type modelType = Type.GetType(SelectedModel); + List pros = null; + if (name == "Searcher") + { + pros = FieldInfos.Where(x => x.IsSearcherField == true).ToList(); + } + if (name == "BatchVM") + { + pros = FieldInfos.Where(x => x.IsBatchField == true).ToList(); + } + foreach (var pro in pros) + { + //对于一对一或者一对多的搜索和批量修改字段,需要在vm中生成对应的变量来获取关联表的数据 + if (pro.InfoType != FieldInfoType.Normal) + { + var subtype = Type.GetType(pro.RelatedField); + if (typeof(TopBasePoco).IsAssignableFrom(subtype) == false || subtype == typeof(FileAttachment)) + { + continue; + } + if (UI == UIEnum.LayUI && IsApi == false) + { + var fname = "All" + pro.FieldName + "s"; + prostring += $@" + public List {fname} {{ get; set; }}"; + initstr += $@" + {fname} = DC.Set<{subtype.Name}>().GetSelectListItems(Wtm, y => y.{pro.SubField});"; + } + } + + //生成普通字段定义 + var proType = modelType.GetSingleProperty(pro.FieldName); + var display = proType.GetCustomAttribute(); + if (display != null) + { + prostring += $@" + [Display(Name = ""{display.Name}"")]"; + } + string typename = proType.PropertyType.Name; + string proname = pro.GetField(DC, modelType); + + switch (pro.InfoType) + { + case FieldInfoType.Normal: + if (proType.PropertyType.IsNullable()) + { + typename = proType.PropertyType.GetGenericArguments()[0].Name + "?"; + } + else if (proType.PropertyType != typeof(string)) + { + typename = proType.PropertyType.Name + "?"; + } + break; + case FieldInfoType.One2Many: + typename = pro.GetFKType(DC, modelType); + if (typename != "string") + { + typename += "?"; + } + break; + case FieldInfoType.Many2Many: + proname = $@"Selected{pro.FieldName}IDs"; + typename = $"List<{pro.GetFKType(DC, modelType)}>"; + break; + default: + break; + } + if ((typename == "DateTime" || typename == "DateTime?") && name == "Searcher") + { + typename = "DateRange"; + } + prostring += $@" + public {typename} {proname} {{ get; set; }}"; + } + rv = rv.Replace("$pros$", prostring).Replace("$init$", initstr); + rv = GetRelatedNamespace(pros, rv); + } + if (name == "ListVM") + { + string headerstring = ""; + string selectstring = ""; + string wherestring = ""; + string subprostring = ""; + string formatstring = ""; + string actionstring = ""; + + if (UI == UIEnum.LayUI && IsApi == false) + { + actionstring = $@" + protected override List InitGridAction() + {{ + return new List + {{ + this.MakeStandardAction(""{ModelName}"", GridActionStandardTypesEnum.Create, Localizer[""Sys.Create""],""{Area ?? ""}"", dialogWidth: 800), + this.MakeStandardAction(""{ModelName}"", GridActionStandardTypesEnum.Edit, Localizer[""Sys.Edit""], ""{Area ?? ""}"", dialogWidth: 800), + this.MakeStandardAction(""{ModelName}"", GridActionStandardTypesEnum.Delete, Localizer[""Sys.Delete""], ""{Area ?? ""}"", dialogWidth: 800), + this.MakeStandardAction(""{ModelName}"", GridActionStandardTypesEnum.Details, Localizer[""Sys.Details""], ""{Area ?? ""}"", dialogWidth: 800), + this.MakeStandardAction(""{ModelName}"", GridActionStandardTypesEnum.BatchEdit, Localizer[""Sys.BatchEdit""], ""{Area ?? ""}"", dialogWidth: 800), + this.MakeStandardAction(""{ModelName}"", GridActionStandardTypesEnum.BatchDelete, Localizer[""Sys.BatchDelete""], ""{Area ?? ""}"", dialogWidth: 800), + this.MakeStandardAction(""{ModelName}"", GridActionStandardTypesEnum.Import, Localizer[""Sys.Import""], ""{Area ?? ""}"", dialogWidth: 800), + this.MakeStandardAction(""{ModelName}"", GridActionStandardTypesEnum.ExportExcel, Localizer[""Sys.Export""], ""{Area ?? ""}""), + }}; + }} +"; + } + + var pros = FieldInfos.Where(x => x.IsListField == true).ToList(); + Type modelType = Type.GetType(SelectedModel); + List existSubPro = new List(); + foreach (var pro in pros) + { + if (pro.InfoType == FieldInfoType.Normal) + { + headerstring += $@" + this.MakeGridHeader(x => x.{pro.FieldName}),"; + if (pro.FieldName.ToLower() != "id") + { + selectstring += $@" + {pro.FieldName} = x.{pro.FieldName},"; + } + } + else + { + var subtype = Type.GetType(pro.RelatedField); + if (subtype == typeof(FileAttachment)) + { + var filefk = DC.GetFKName2(modelType, pro.FieldName); + headerstring += $@" + this.MakeGridHeader(x => x.{filefk}).SetFormat({filefk}Format),"; + selectstring += $@" + {filefk} = x.{filefk},"; + formatstring += GetResource("HeaderFormat.txt").Replace("$modelname$", ModelName).Replace("$field$", filefk).Replace("$classname$", $"{ModelName}{(IsApi == true ? "Api" : "")}"); + } + else + { + var subpro = subtype.GetSingleProperty(pro.SubField); + existSubPro.Add(subpro); + string prefix = ""; + int count = existSubPro.Where(x => x.Name == subpro.Name).Count(); + if (count > 1) + { + prefix = count + ""; + } + string subtypename = subpro.PropertyType.Name; + if (subpro.PropertyType.IsNullable()) + { + subtypename = subpro.PropertyType.GetGenericArguments()[0].Name + "?"; + } + + var subdisplay = subpro.GetCustomAttribute(); + headerstring += $@" + this.MakeGridHeader(x => x.{pro.SubField + "_view" + prefix}),"; + if (pro.InfoType == FieldInfoType.One2Many) + { + selectstring += $@" + {pro.SubField + "_view" + prefix} = x.{pro.FieldName}.{pro.SubField},"; + } + else + { + var middleType = modelType.GetSingleProperty(pro.FieldName).PropertyType.GenericTypeArguments[0]; + var middlename = DC.GetPropertyNameByFk(middleType, pro.SubIdField); + if(typeof(IPersistPoco).IsAssignableFrom(Type.GetType(pro.RelatedField))) + { + selectstring += $@" + {pro.SubField + "_view" + prefix} = x.{pro.FieldName}.Where(y=>y.{middlename}.IsValid==true).Select(y=>y.{middlename}.{pro.SubField}).ToSepratedString(null,"",""), "; + } + else + { + selectstring += $@" + {pro.SubField + "_view" + prefix} = x.{pro.FieldName}.Select(y=>y.{middlename}.{pro.SubField}).ToSepratedString(null,"",""), "; + } + } + if (subdisplay?.Name != null) + { + subprostring += $@" + [Display(Name = ""{subdisplay.Name}"")]"; + } + subprostring += $@" + public {subtypename} {pro.SubField + "_view" + prefix} {{ get; set; }}"; + } + } + + } + var wherepros = FieldInfos.Where(x => x.IsSearcherField == true).ToList(); + foreach (var pro in wherepros) + { + if (pro.SubField == "`file") + { + continue; + } + var proType = modelType.GetSingleProperty(pro.FieldName)?.PropertyType; + + switch (pro.InfoType) + { + case FieldInfoType.Normal: + if (proType == typeof(string)) + { + wherestring += $@" + .CheckContain(Searcher.{pro.FieldName}, x=>x.{pro.FieldName})"; + } + else if (proType == typeof(DateTime) || proType == typeof(DateTime?)) + { + wherestring += $@" + .CheckBetween(Searcher.{pro.FieldName}?.GetStartTime(), Searcher.{pro.FieldName}?.GetEndTime(), x => x.{pro.FieldName}, includeMax: false)"; + } + else + { + wherestring += $@" + .CheckEqual(Searcher.{pro.FieldName}, x=>x.{pro.FieldName})"; + } + break; + case FieldInfoType.One2Many: + var fk = DC.GetFKName2(modelType, pro.FieldName); + wherestring += $@" + .CheckEqual(Searcher.{fk}, x=>x.{fk})"; + break; + case FieldInfoType.Many2Many: + var subtype = Type.GetType(pro.RelatedField); + var fk2 = DC.GetFKName(modelType, pro.FieldName); + wherestring += $@" + .CheckWhere(Searcher.Selected{pro.FieldName}IDs,x=>DC.Set<{proType.GetGenericArguments()[0].Name}>().Where(y=>Searcher.Selected{pro.FieldName}IDs.Contains(y.{pro.SubIdField})).Select(z=>z.{fk2}).Contains(x.ID))"; + break; + default: + break; + } + } + rv = rv.Replace("$headers$", headerstring).Replace("$where$", wherestring).Replace("$select$", selectstring).Replace("$subpros$", subprostring).Replace("$format$", formatstring).Replace("$actions$", actionstring); + rv = GetRelatedNamespace(pros, rv); + } + if (name == "CrudVM") + { + string prostr = ""; + string initstr = ""; + string includestr = ""; + string addstr = ""; + string editstr = ""; + var pros = FieldInfos.Where(x => x.IsFormField == true && string.IsNullOrEmpty(x.RelatedField) == false).ToList(); + foreach (var pro in pros) + { + var subtype = Type.GetType(pro.RelatedField); + if (typeof(TopBasePoco).IsAssignableFrom(subtype) == false || subtype == typeof(FileAttachment)) + { + continue; + } + var fname = "All" + pro.FieldName + "s"; + if (UI == UIEnum.LayUI) + { + prostr += $@" + public List {fname} {{ get; set; }}"; + initstr += $@" + {fname} = DC.Set<{subtype.Name}>().GetSelectListItems(Wtm, y => y.{pro.SubField});"; + } + includestr += $@" + SetInclude(x => x.{pro.FieldName});"; + + if (pro.InfoType == FieldInfoType.Many2Many) + { + Type modelType = Type.GetType(SelectedModel); + var protype = modelType.GetSingleProperty(pro.FieldName); + prostr += $@" + [Display(Name = ""{protype.GetPropertyDisplayName()}"")] + public List Selected{pro.FieldName}IDs {{ get; set; }}"; + initstr += $@" + Selected{pro.FieldName}IDs = Entity.{pro.FieldName}?.Select(x => x.{pro.SubIdField}.ToString()).ToList();"; + addstr += $@" + Entity.{pro.FieldName} = new List<{protype.PropertyType.GetGenericArguments()[0].Name}>(); + if (Selected{pro.FieldName}IDs != null) + {{ + foreach (var id in Selected{pro.FieldName}IDs) + {{ + {protype.PropertyType.GetGenericArguments()[0].Name} middle = new {protype.PropertyType.GetGenericArguments()[0].Name}(); + middle.SetPropertyValue(""{pro.SubIdField}"", id); + Entity.{pro.FieldName}.Add(middle); + }} + }} +"; + editstr += $@" + Entity.{pro.FieldName} = new List<{protype.PropertyType.GetGenericArguments()[0].Name}>(); + if(Selected{pro.FieldName}IDs != null ) + {{ + foreach (var item in Selected{pro.FieldName}IDs) + {{ + {protype.PropertyType.GetGenericArguments()[0].Name} middle = new {protype.PropertyType.GetGenericArguments()[0].Name}(); + middle.SetPropertyValue(""{pro.SubIdField}"", item); + Entity.{pro.FieldName}.Add(middle); + }} + }} +"; + } + } + if ((UI == UIEnum.LayUI && IsApi == false) || UI == UIEnum.Blazor) + { + rv = rv.Replace("$pros$", prostr).Replace("$init$", initstr).Replace("$include$", includestr).Replace("$add$", addstr).Replace("$edit$", editstr); + } + else + { + rv = rv.Replace("$pros$", "").Replace("$init$", "").Replace("$include$", includestr).Replace("$add$", "").Replace("$edit$", ""); + } + rv = GetRelatedNamespace(pros, rv); + } + if (name == "ImportVM") + { + string prostring = ""; + string initstr = ""; + Type modelType = Type.GetType(SelectedModel); + List pros = FieldInfos.Where(x => x.IsImportField == true).ToList(); + foreach (var pro in pros) + { + if (pro.InfoType == FieldInfoType.Many2Many) + { + continue; + } + + if (string.IsNullOrEmpty(pro.RelatedField) == false) + { + var subtype = Type.GetType(pro.RelatedField); + if (typeof(TopBasePoco).IsAssignableFrom(subtype) == false || subtype == typeof(FileAttachment)) + { + continue; + } + initstr += $@" + {pro.FieldName + "_Excel"}.DataType = ColumnDataType.ComboBox; + {pro.FieldName + "_Excel"}.ListItems = DC.Set<{subtype.Name}>().GetSelectListItems(Wtm, y => y.{pro.SubField});"; + } + var proType = modelType.GetSingleProperty(pro.FieldName); + var display = proType.GetCustomAttribute(); + var filefk = DC.GetFKName2(modelType, pro.FieldName); + if (display != null) + { + prostring += $@" + [Display(Name = ""{display.Name}"")]"; + } + if (string.IsNullOrEmpty(pro.RelatedField) == false) + { + prostring += $@" + public ExcelPropety {pro.FieldName + "_Excel"} = ExcelPropety.CreateProperty<{ModelName}>(x => x.{filefk});"; + } + else + { + prostring += $@" + public ExcelPropety {pro.FieldName + "_Excel"} = ExcelPropety.CreateProperty<{ModelName}>(x => x.{pro.FieldName});"; + } + } + rv = rv.Replace("$pros$", prostring).Replace("$init$", initstr); + rv = GetRelatedNamespace(pros, rv); + + } + return rv; + } + + public string GenerateView(string name) + { + var rv = GetResource($"{name}.txt", "Mvc").Replace("$vmnamespace$", VMNs).Replace("$modelname$", ModelName); + if (name == "CreateView" || name == "EditView" || name == "DeleteView" || name == "DetailsView" || name == "BatchEditView") + { + StringBuilder fieldstr = new StringBuilder(); + string pre = ""; + List pros = null; + if (name == "BatchEditView") + { + pros = FieldInfos.Where(x => x.IsBatchField == true).ToList(); + pre = "LinkedVM"; + } + else + { + pros = FieldInfos.Where(x => x.IsFormField == true).ToList(); + pre = "Entity"; + } + Type modelType = Type.GetType(SelectedModel); + fieldstr.Append(Environment.NewLine); + fieldstr.Append(@""); + fieldstr.Append(Environment.NewLine); + foreach (var item in pros) + { + if (name == "DeleteView" || name == "DetailsView") + { + if (string.IsNullOrEmpty(item.SubIdField) == true) + { + if (string.IsNullOrEmpty(item.RelatedField) == false && item.SubField != "`file") + { + fieldstr.Append($@""); + } + else + { + string idname = item.FieldName; + if (string.IsNullOrEmpty(item.RelatedField) == false && item.SubField == "`file") + { + var filefk = DC.GetFKName2(modelType, item.FieldName); + idname = filefk; + } + fieldstr.Append($@""); + } + } + } + else + { + if (string.IsNullOrEmpty(item.RelatedField) == false) + { + var filefk = DC.GetFKName2(modelType, item.FieldName); + if (item.SubField == "`file") + { + if (name != "BatchEditView") + { + fieldstr.Append($@""); + } + } + else + { + var fname = "All" + item.FieldName + "s"; + if (name == "BatchEditView") + { + fname = "LinkedVM." + fname; + } + if (string.IsNullOrEmpty(item.SubIdField)) + { + fieldstr.Append($@""); + } + else + { + if (name == "BatchEditView") + { + fieldstr.Append($@""); + } + else + { + fieldstr.Append($@""); + } + } + } + } + else + { + var proType = modelType.GetSingleProperty(item.FieldName)?.PropertyType; + Type checktype = proType; + if (proType.IsNullable()) + { + checktype = proType.GetGenericArguments()[0]; + } + if(checktype == typeof(bool) && proType.IsNullable() == false) + { + fieldstr.Append($@""); + + } + else if (checktype == typeof(bool) || checktype.IsEnum()) + { + fieldstr.Append($@""); + } + else if (checktype.IsPrimitive || checktype == typeof(string) || checktype == typeof(decimal)) + { + fieldstr.Append($@""); + } + else if (checktype == typeof(DateTime)) + { + fieldstr.Append($@""); + } + } + } + fieldstr.Append(Environment.NewLine); + } + fieldstr.Append(""); + rv = rv.Replace("$fields$", fieldstr.ToString()); + } + if (name == "ListView") + { + StringBuilder fieldstr = new StringBuilder(); + var pros = FieldInfos.Where(x => x.IsSearcherField == true).ToList(); + Type modelType = Type.GetType(SelectedModel); + fieldstr.Append(Environment.NewLine); + fieldstr.Append(@""); + fieldstr.Append(Environment.NewLine); + foreach (var item in pros) + { + if (string.IsNullOrEmpty(item.RelatedField) == false) + { + if (item.SubField == "`file") + { + continue; + } + var fname = "All" + item.FieldName + "s"; + var fk = ""; + if (string.IsNullOrEmpty(item.SubIdField)) + { + fk = DC.GetFKName2(modelType, item.FieldName); ; + } + else + { + fk = $@"Selected{item.FieldName}IDs"; + } + fieldstr.Append($@""); + } + else + { + var proType = modelType.GetSingleProperty(item.FieldName)?.PropertyType; + Type checktype = proType; + if (proType.IsNullable()) + { + checktype = proType.GetGenericArguments()[0]; + } + if ((checktype.IsPrimitive && checktype != typeof(bool)) || checktype == typeof(string) || checktype == typeof(decimal)) + { + fieldstr.Append($@""); + } + if (checktype == typeof(DateTime)) + { + fieldstr.Append($@""); + } + if (checktype.IsEnum() || checktype.IsBool()) + { + fieldstr.Append($@""); + } + } + fieldstr.Append(Environment.NewLine); + } + fieldstr.Append($@""); + string url = ""; + if (string.IsNullOrEmpty(Area)) + { + url = $"/{ModelName}/Search"; + } + else + { + url = $"/{Area}/{ModelName}/Search"; + } + rv = rv.Replace("$fields$", fieldstr.ToString()).Replace("$searchurl$", url); + } + return rv; + } + + public string GenerateTest() + { + var rv = ""; + if (TestDir != null) + { + Type modelType = Type.GetType(SelectedModel); + if (UI == UIEnum.LayUI && IsApi == false) + { + if (typeof(IBasePoco).IsAssignableFrom( modelType) || typeof(IPersistPoco).IsAssignableFrom(modelType)) + { + rv = GetResource($"ControllerTest.txt").Replace("$cns$", ControllerNs).Replace("$tns$", TestNs).Replace("$vns$", VMNs).Replace("$model$", ModelName).Replace("$mns$", ModelNS).Replace("$dns$", DataNs); + } + else + { + rv = GetResource($"ControllerTestTopPoco.txt").Replace("$cns$", ControllerNs).Replace("$tns$", TestNs).Replace("$vns$", VMNs).Replace("$model$", ModelName).Replace("$mns$", ModelNS).Replace("$dns$", DataNs); + } + } + else + { + if (typeof(IBasePoco).IsAssignableFrom(modelType) || typeof(IPersistPoco).IsAssignableFrom(modelType)) + { + rv = GetResource($"ApiTest.txt").Replace("$cns$", ControllerNs).Replace("$tns$", TestNs).Replace("$vns$", VMNs).Replace("$model$", ModelName).Replace("$mns$", ModelNS).Replace("$dns$", DataNs).Replace("$classnamel$", $"{ModelName}{(IsApi == true ? "Api" : "")}"); + } + else + { + rv = GetResource($"ApiTestTopPoco.txt").Replace("$cns$", ControllerNs).Replace("$tns$", TestNs).Replace("$vns$", VMNs).Replace("$model$", ModelName).Replace("$mns$", ModelNS).Replace("$dns$", DataNs).Replace("$classnamel$", $"{ModelName}{(IsApi == true ? "Api" : "")}"); + } + } + var modelprops = modelType.GetRandomValues(); + var batchpros = FieldInfos.Where(x => x.IsBatchField == true).ToList(); + string cpros = ""; + string epros = ""; + string pros = ""; + string mpros = ""; + string assert = ""; + string eassert = ""; + string fc = ""; + string add = ""; + string linkedpros = ""; + string linkedfc = ""; + string meassert = ""; + List addexist = new List(); + foreach (var pro in modelprops) + { + if (pro.Value == "$fk$") + { + var fktype = modelType.GetSingleProperty(pro.Key[0..^2])?.PropertyType; + add += GenerateAddFKModel(pro.Key[0..^2], fktype, addexist); + } + } + + foreach (var pro in modelprops) + { + if (pro.Value == "$fk$") + { + var fktype = modelType.GetSingleProperty(pro.Key[0..^2])?.PropertyType; + cpros += $@" + v.{pro.Key} = Add{fktype.Name}();"; + pros += $@" + v.{pro.Key} = Add{fktype.Name}();"; + mpros += $@" + v1.{pro.Key} = Add{fktype.Name}();"; + } + else + { + cpros += $@" + v.{pro.Key} = {pro.Value};"; + pros += $@" + v.{pro.Key} = {pro.Value};"; + mpros += $@" + v1.{pro.Key} = {pro.Value};"; + assert += $@" + Assert.AreEqual(data.{pro.Key}, {pro.Value});"; + } + fc += $@" + vm.FC.Add(""Entity.{pro.Key}"", """");"; + + } + + var modelpros2 = modelType.GetRandomValues(); + foreach (var pro in modelpros2) + { + //if (pro.Key.ToLower() == "id") + //{ + // continue; + //} + + if (pro.Value == "$fk$") + { + mpros += $@" + v2.{ pro.Key} = v1.{pro.Key}; "; + + } + else + { + mpros += $@" + v2.{pro.Key} = {pro.Value};"; + if (pro.Key.ToLower() != "id") + { + epros += $@" + v.{pro.Key} = {pro.Value};"; + eassert += $@" + Assert.AreEqual(data.{pro.Key}, {pro.Value});"; + } + } + } + + var modelpros3 = modelType.GetRandomValues(); + foreach (var pro in modelpros3) + { + if (batchpros.Any(x => x.FieldName == pro.Key) && pro.Key.ToLower() != "id") + { + linkedpros += $@" + vm.LinkedVM.{pro.Key} = {pro.Value};"; + linkedfc += $@" + vm.FC.Add(""LinkedVM.{pro.Key}"", """");"; + meassert += $@" + Assert.AreEqual(data1.{pro.Key}, {pro.Value});"; + meassert += $@" + Assert.AreEqual(data2.{pro.Key}, {pro.Value});"; + } + } + + + string del = $"Assert.AreEqual(data, null);"; + string mdel = @"Assert.AreEqual(data1, null); + Assert.AreEqual(data2, null);"; + if (typeof(IPersistPoco).IsAssignableFrom( modelType)) + { + del = $"Assert.AreEqual(data.IsValid, false);"; + mdel = @"Assert.AreEqual(data1.IsValid, false); + Assert.AreEqual(data2.IsValid, false);"; + } + + rv = rv.Replace("$cpros$", cpros).Replace("$epros$", epros).Replace("$pros$", pros).Replace("$mpros$", mpros) + .Replace("$assert$", assert).Replace("$eassert$", eassert).Replace("$fc$", fc).Replace("$add$", add).Replace("$del$", del).Replace("$mdel$", mdel) + .Replace("$linkedpros$", linkedpros).Replace("$linkedfc$", linkedfc).Replace("$meassert$", meassert); + + rv = GetRelatedNamespace(FieldInfos.Where(x=>string.IsNullOrEmpty( x.RelatedField) == false).ToList(), rv); + } + return rv; + } + + private string GenerateAddFKModel(string keyname, Type t, List exist) + { + if(exist == null) + { + exist = new List(); + } + if(exist.Contains(t) == true) + { + return ""; + } + exist.Add(t); + var modelprops = t.GetRandomValues(); + var mname = t.Name?.Split(',').FirstOrDefault()?.Split('.').LastOrDefault() ?? ""; + string cpros = ""; + string rv = ""; + foreach (var pro in modelprops) + { + if (pro.Value == "$fk$") + { + var fktype = t.GetSingleProperty(pro.Key[0..^2])?.PropertyType; + if (fktype != t) + { + rv += GenerateAddFKModel(pro.Key[0..^2], fktype, exist); + } + } + } + + + foreach (var pro in modelprops) + { + if (pro.Value == "$fk$") + { + var fktype = t.GetSingleProperty(pro.Key[0..^2])?.PropertyType; + if (fktype != t) + { + cpros += $@" + v.{pro.Key} = Add{fktype.Name}();"; + } + } + else + { + cpros += $@" + v.{pro.Key} = {pro.Value};"; + } + } + var idpro = t.GetSingleProperty("ID"); + rv += $@" + private {idpro.PropertyType.Name} Add{t.Name}() + {{ + {mname} v = new {mname}(); + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + {{ + try{{ +{cpros} + context.Set<{mname}>().Add(v); + context.SaveChanges(); + }} + catch{{}} + }} + return v.ID; + }} +"; + return rv; + } + + public string GenerateReactView(string name) + { + var rv = GetResource($"{name}.txt", "Spa.React.views") + .Replace("$modelname$", ModelName.ToLower()); + Type modelType = Type.GetType(SelectedModel); + if (name == "table") + { + StringBuilder fieldstr = new StringBuilder(); + var pros = FieldInfos.Where(x => x.IsListField == true).ToList(); + fieldstr.Append(Environment.NewLine); + List existSubPro = new List(); + int rowheight = 30; + for (int i = 0; i < pros.Count; i++) + { + var item = pros[i]; + var mpro = modelType.GetSingleProperty(item.FieldName); + string label = mpro.GetPropertyDisplayName(); + string render = ""; + string newname = item.FieldName; + if (mpro.PropertyType.IsBoolOrNullableBool()) + { + render = "columnsRenderBoolean"; + } + if (string.IsNullOrEmpty(item.RelatedField) == false) + { + var subtype = Type.GetType(item.RelatedField); + string prefix = ""; + if (subtype == typeof(FileAttachment)) + { + if (item.FieldName.ToLower().Contains("photo") || item.FieldName.ToLower().Contains("pic") || item.FieldName.ToLower().Contains("icon")) + { + render = "columnsRenderImg"; + rowheight = 110; + } + else + { + render = "columnsRenderDownload"; + } + var fk = DC.GetFKName2(modelType, item.FieldName); + newname = fk; + } + else + { + var subpro = subtype.GetSingleProperty(item.SubField); + existSubPro.Add(subpro); + int count = existSubPro.Where(x => x.Name == subpro.Name).Count(); + if (count > 1) + { + prefix = count + ""; + } + newname = item.SubField + "_view" + prefix; + } + } + fieldstr.Append($@" + {{ + field: ""{newname}"", + headerName: ""{label}"""); + + if (render != "") + { + fieldstr.Append($@", + cellRenderer: ""{render}"" "); + } + fieldstr.Append($@" + }}"); + if (i < pros.Count - 1) + { + fieldstr.Append(','); + } + fieldstr.Append(Environment.NewLine); + } + return rv.Replace("$columns$", fieldstr.ToString()).Replace("$rowheight$", rowheight.ToString()); + } + if (name == "models") + { + StringBuilder fieldstr = new StringBuilder(); + StringBuilder fieldstr2 = new StringBuilder(); + var pros = FieldInfos.Where(x => x.IsFormField == true).ToList(); + var pros2 = FieldInfos.Where(x => x.IsSearcherField == true).ToList(); + + //生成表单model + for (int i = 0; i < pros.Count; i++) + { + var item = pros[i]; + var property = modelType.GetSingleProperty(item.FieldName); + string label = property.GetPropertyDisplayName(); + bool isrequired = property.IsPropertyRequired(); + var fktest = DC.GetFKName2(modelType, item.FieldName); + if (string.IsNullOrEmpty(fktest) == false) + { + isrequired = modelType.GetSingleProperty(fktest).IsPropertyRequired(); + } + string rules = "rules: []"; + if (isrequired == true) + { + rules = $@"rules: [{{ ""required"": true, ""message"": }}]"; + } + fieldstr.AppendLine($@" /** {label} */"); + if (string.IsNullOrEmpty(item.RelatedField) == false && string.IsNullOrEmpty(item.SubIdField) == true) + { + var fk = DC.GetFKName2(modelType, item.FieldName); + fieldstr.AppendLine($@" ""Entity.{fk}"":{{"); + } + else + { + fieldstr.AppendLine($@" ""Entity.{item.FieldName}"":{{"); + } + fieldstr.AppendLine($@" label: ""{label}"","); + fieldstr.AppendLine($@" {rules},"); + if (string.IsNullOrEmpty(item.RelatedField) == false) + { + var subtype = Type.GetType(item.RelatedField); + if (item.SubField == "`file") + { + fieldstr.AppendLine($@" formItem: "); + } + else + { + if (string.IsNullOrEmpty(item.SubIdField) == true) + { + fieldstr.AppendLine($@" formItem: "); + } + else + { + fieldstr.AppendLine($@" formItem: "); + + } + } + } + else + { + var proType = modelType.GetSingleProperty(item.FieldName)?.PropertyType; + Type checktype = proType; + if (proType.IsNullable()) + { + checktype = proType.GetGenericArguments()[0]; + } + if (checktype == typeof(bool)) + { + fieldstr.AppendLine($@" formItem: }} unCheckedChildren={{}} />"); + } + else if (checktype.IsEnum()) + { + var es = checktype.ToListItems(); + fieldstr.AppendLine($@" formItem: "); + } + else if (checktype.IsNumber()) + { + fieldstr.AppendLine($@" formItem: "); + } + else if (checktype == typeof(string)) + { + fieldstr.AppendLine($@" formItem: "); + } + else if (checktype == typeof(DateTime)) + { + fieldstr.AppendLine($@" formItem: "); + } + } + fieldstr.Append(" }"); + if (i < pros.Count - 1) + { + fieldstr.Append(','); + } + fieldstr.Append(Environment.NewLine); + } + + //生成searchmodel + for (int i = 0; i < pros2.Count; i++) + { + var item = pros2[i]; + if (item.SubField == "`file") + { + continue; + } + var property = modelType.GetSingleProperty(item.FieldName); + string label = property.GetPropertyDisplayName(); + string rules = "rules: []"; + + fieldstr2.AppendLine($@" /** {label} */"); + if (string.IsNullOrEmpty(item.RelatedField) == false) + { + if (string.IsNullOrEmpty(item.SubIdField) == true) + { + var fk = DC.GetFKName2(modelType, item.FieldName); + fieldstr2.AppendLine($@" ""{fk}"":{{"); + } + else + { + fieldstr2.AppendLine($@" ""Selected{item.FieldName}IDs"":{{"); + } + } + else + { + fieldstr2.AppendLine($@" ""{item.FieldName}"":{{"); + } + fieldstr2.AppendLine($@" label: ""{label}"","); + fieldstr2.AppendLine($@" {rules},"); + if (string.IsNullOrEmpty(item.RelatedField) == false) + { + var subtype = Type.GetType(item.RelatedField); + if (string.IsNullOrEmpty(item.SubIdField) == true) + { + fieldstr2.AppendLine($@" formItem: "); + } + else + { + fieldstr2.AppendLine($@" formItem: "); + + } + } + else + { + var proType = modelType.GetSingleProperty(item.FieldName)?.PropertyType; + Type checktype = proType; + if (proType.IsNullable()) + { + checktype = proType.GetGenericArguments()[0]; + } + if (checktype == typeof(bool)) + { + fieldstr2.AppendLine($@" formItem: , Value: true }},{{ Text: , Value: false }} + ]}}/>"); + } + else if (checktype.IsEnum()) + { + var es = checktype.ToListItems(); + fieldstr2.AppendLine($@" formItem: "); + } + else if (checktype.IsNumber()) + { + fieldstr2.AppendLine($@" formItem: "); + } + else if (checktype == typeof(string)) + { + fieldstr2.AppendLine($@" formItem: "); + } + else if (checktype == typeof(DateTime)) + { + fieldstr2.AppendLine($@" formItem: "); + } + } + fieldstr2.Append(" }"); + if (i < pros.Count - 1) + { + fieldstr2.Append(','); + } + fieldstr2.Append(Environment.NewLine); + } + + return rv.Replace("$fields$", fieldstr.ToString()).Replace("$fields2$", fieldstr2.ToString()); + } + + if (name == "search") + { + return rv; + } + if (name == "forms") + { + StringBuilder fieldstr = new StringBuilder(); + var pros = FieldInfos.Where(x => x.IsFormField == true).ToList(); + + for (int i = 0; i < pros.Count; i++) + { + var item = pros[i]; + if (string.IsNullOrEmpty(item.SubIdField)) + { + if (string.IsNullOrEmpty(item.RelatedField) == false) + { + var fk = DC.GetFKName2(modelType, item.FieldName); + fieldstr.AppendLine($@" "); + } + else + { + var proType = modelType.GetSingleProperty(item.FieldName)?.PropertyType; + Type checktype = proType; + if (proType.IsNullable()) + { + checktype = proType.GetGenericArguments()[0]; + } + if (checktype == typeof(bool)) + { + fieldstr.AppendLine($@" "); + } + else + { + fieldstr.AppendLine($@" "); + } + } + } + else + { + fieldstr.AppendLine($@" + + "); + } + } + return rv.Replace("$fields$", fieldstr.Replace("$switchdefaultvalue$", "value={false}").ToString()).Replace("$efields$", fieldstr.Replace("$switchdefaultvalue$", "").ToString()); + } + + return rv; + } + + + public string GenerateVUEView(string name, List apineeded) + { + var rv = GetResource($"{name}.txt", "Spa.Vue") + .Replace("$modelname$", ModelName.ToLower()); + if (apineeded == null) + { + apineeded = new List(); + } + Type modelType = Type.GetType(SelectedModel); + if (name == "config") + { + StringBuilder fieldstr = new StringBuilder(); + StringBuilder enumstr = new StringBuilder(); + var pros = FieldInfos.Where(x => x.IsListField == true || x.IsSearcherField == true).ToList(); + fieldstr.Append(Environment.NewLine); + List existSubPro = new List(); + List existEnum = new List(); + int rowheight = 30; + for (int i = 0; i < pros.Count; i++) + { + var item = pros[i]; + var mpro = modelType.GetSingleProperty(item.FieldName); + string label = mpro.GetPropertyDisplayName(); + string render = ""; + string newname = item.FieldName; + if (mpro.PropertyType.IsBoolOrNullableBool()) + { + render = "columnsRenderBoolean"; + } + if (string.IsNullOrEmpty(item.RelatedField) == false) + { + var subtype = Type.GetType(item.RelatedField); + string prefix = ""; + if (subtype == typeof(FileAttachment)) + { + if (item.FieldName.ToLower().Contains("photo") || item.FieldName.ToLower().Contains("pic") || item.FieldName.ToLower().Contains("icon")) + { + render = "columnsRenderImg"; + rowheight = 110; + } + else + { + render = "columnsRenderDownload"; + } + var fk = DC.GetFKName2(modelType, item.FieldName); + newname = fk; + } + else + { + var subpro = subtype.GetSingleProperty(item.SubField); + existSubPro.Add(subpro); + int count = existSubPro.Where(x => x.Name == subpro.Name).Count(); + if (count > 1) + { + prefix = count + ""; + } + newname = item.SubField + "_view" + prefix; + } + } + + else + { + var proType = modelType.GetSingleProperty(item.FieldName)?.PropertyType; + Type checktype = proType; + if (proType.IsNullable()) + { + checktype = proType.GetGenericArguments()[0]; + } + if (checktype.IsEnum()) + { + if (existEnum.Contains(checktype.Name) == false) + { + var es = checktype.ToListItems(); + enumstr.AppendLine($@"export const {item.FieldName}Types: Array = ["); + for (int a = 0; a < es.Count; a++) + { + var e = es[a]; + enumstr.Append($@" {{ Text: ""{e.Text}"", Value: ""{e.Value}"" }}"); + if (a < es.Count - 1) + { + enumstr.Append(','); + } + enumstr.AppendLine(); + } + enumstr.AppendLine($@"];"); + existEnum.Add(checktype.Name); + } + } + } + fieldstr.Append($@" + {{ + key: ""{newname}"", + label: ""{label}"""); + + if (render != "") + { + fieldstr.Append($@", + isSlot: true "); + } + fieldstr.Append($@" + }}"); + fieldstr.Append(','); + } + return rv.Replace("$fields$", fieldstr.ToString()).Replace("$rowheight$", rowheight.ToString()).Replace("$enums$", enumstr.ToString()); + } + if (name == "views.dialog-form") + { + StringBuilder fieldstr = new StringBuilder(); + List actions = new List(); + List enums = new List(); + var pros = FieldInfos.Where(x => x.IsFormField == true).ToList(); + + //生成表单model + for (int i = 0; i < pros.Count; i++) + { + var item = pros[i]; + var property = modelType.GetSingleProperty(item.FieldName); + string label = property.GetPropertyDisplayName(); + bool isrequired = property.IsPropertyRequired(); + var fktest = DC.GetFKName2(modelType, item.FieldName); + if (string.IsNullOrEmpty(fktest) == false) + { + isrequired = modelType.GetSingleProperty(fktest).IsPropertyRequired(); + } + string rules = "rules: []"; + if (isrequired == true) + { + rules = $@"rules: [{{ required: true, message: ""{label}""+this.$t(""form.notnull""),trigger: ""blur"" }}]"; + } + if (string.IsNullOrEmpty(item.RelatedField) == false && string.IsNullOrEmpty(item.SubIdField) == true) + { + var fk = DC.GetFKName2(modelType, item.FieldName); + fieldstr.AppendLine($@" ""Entity.{fk}"":{{"); + } + else + { + fieldstr.AppendLine($@" ""Entity.{item.FieldName}"":{{"); + } + fieldstr.AppendLine($@" label: ""{label}"","); + fieldstr.AppendLine($@" {rules},"); + if (string.IsNullOrEmpty(item.RelatedField) == false) + { + var subtype = Type.GetType(item.RelatedField); + if (item.SubField == "`file") + { + if (item.FieldName.ToLower().Contains("photo") || item.FieldName.ToLower().Contains("pic") || item.FieldName.ToLower().Contains("icon")) + { + fieldstr.AppendLine($@" type: ""wtmUploadImg"", + props: {{ + isHead: true, + imageStyle: {{ width: ""100px"", height: ""100px"" }} + }} +"); + } + else + { + fieldstr.AppendLine($@" type: ""upload"""); + } + } + else + { + if (string.IsNullOrEmpty(item.SubIdField) == true) + { + fieldstr.AppendLine($@" type: ""select"", + children: this.get{subtype.Name}Data, + props: {{ + clearable: true + }}"); + } + else + { + fieldstr.AppendLine($@" type: ""transfer"", + mapKey: ""{item.SubIdField}"", + props: {{ + data: this.get{subtype.Name}Data.map(item => ({{ + key: item.Value, + label: item.Text + }})), + titles: [this.$t(""form.all""), this.$t(""form.selected"")], + filterable: true, + filterMethod: filterMethod + }}, + span: 24, + defaultValue: []"); + + } + apineeded.Add($"get{subtype.Name}"); + actions.Add($"get{subtype.Name}"); + } + } + else + { + var proType = modelType.GetSingleProperty(item.FieldName)?.PropertyType; + Type checktype = proType; + if (proType.IsNullable()) + { + checktype = proType.GetGenericArguments()[0]; + } + if (checktype == typeof(bool)) + { + fieldstr.AppendLine($@" type: ""switch"""); + } + else if (checktype.IsEnum()) + { + fieldstr.AppendLine($@" type: ""select"", + children: {item.FieldName}Types, + props: {{ + clearable: true + }}"); + + enums.Add(item.FieldName + "Types"); + } + else if (checktype.IsNumber()) + { + fieldstr.AppendLine($@" type: ""input"""); + } + else if (checktype == typeof(string)) + { + fieldstr.AppendLine($@" type: ""input"""); + } + else if (checktype == typeof(DateTime)) + { + fieldstr.AppendLine($@" type: ""datePicker"""); + } + } + fieldstr.Append(" }"); + if (i < pros.Count - 1) + { + fieldstr.Append(','); + } + fieldstr.Append(Environment.NewLine); + } + string a1 = ""; + string a2 = ""; + foreach (var item in actions.Distinct()) + { + a1 += $@" @Action + {item}; + @State + {item}Data; +"; + a2 += $@" this.{item}(); +"; + } + string import = ""; + if (enums.Count > 0) + { + import = $@"import {{ {enums.Distinct().ToSepratedString()} }} from ""../config"";"; + } + return rv.Replace("$fields$", fieldstr.ToString()).Replace("$actions$", a1).Replace("$runactions$", a2).Replace("$import$", import); + } + + if (name == "index") + { + StringBuilder fieldstr2 = new StringBuilder(); + StringBuilder actions = new StringBuilder(); + List acts = new List(); + List enums = new List(); + var pros2 = FieldInfos.Where(x => x.IsSearcherField == true || x.IsListField).ToList(); + int searchcount = 0; + for (int i = 0; i < pros2.Count; i++) + { + + var item = pros2[i]; + if (item.IsListField == true) + { + var mpro = modelType.GetSingleProperty(item.FieldName); + if (mpro.PropertyType.IsBoolOrNullableBool()) + { + actions.AppendLine($@" +"); + } + if (string.IsNullOrEmpty(item.RelatedField) == false) + { + var subtype = Type.GetType(item.RelatedField); + var fk = DC.GetFKName2(modelType, item.FieldName); + if (subtype == typeof(FileAttachment)) + { + if (item.FieldName.ToLower().Contains("photo") || item.FieldName.ToLower().Contains("pic") || item.FieldName.ToLower().Contains("icon")) + { + actions.AppendLine($@" +"); + } + else + { + actions.AppendLine($@" +"); + } + } + } + } + if (item.IsSearcherField == true) + { + if (item.SubField == "`file") + { + continue; + } + searchcount++; + var property = modelType.GetSingleProperty(item.FieldName); + string label = property.GetPropertyDisplayName(); + string rules = "rules: []"; + + if (string.IsNullOrEmpty(item.RelatedField) == false) + { + if (string.IsNullOrEmpty(item.SubIdField) == true) + { + var fk = DC.GetFKName2(modelType, item.FieldName); + fieldstr2.AppendLine($@" ""{fk}"":{{"); + } + else + { + fieldstr2.AppendLine($@" ""Selected{item.FieldName}IDs"":{{"); + } + } + else + { + fieldstr2.AppendLine($@" ""{item.FieldName}"":{{"); + } + fieldstr2.AppendLine($@" label: ""{label}"","); + fieldstr2.AppendLine($@" {rules},"); + if (string.IsNullOrEmpty(item.RelatedField) == false) + { + var subtype = Type.GetType(item.RelatedField); + if (string.IsNullOrEmpty(item.SubIdField) == true) + { + fieldstr2.AppendLine($@" type: ""select"", + children: this.get{subtype.Name}Data, + props: {{ + clearable: true, + placeholder: '全部' + }}"); + + } + else + { + fieldstr2.AppendLine($@" type: ""select"", + children: this.get{subtype.Name}Data, + props: {{ + clearable: true , + multiple: true, + ""collapse-tags"": true + }}"); + + } + apineeded.Add($"get{subtype.Name}"); + acts.Add($"get{subtype.Name}"); + } + else + { + var proType = modelType.GetSingleProperty(item.FieldName)?.PropertyType; + Type checktype = proType; + if (proType.IsNullable()) + { + checktype = proType.GetGenericArguments()[0]; + } + if (checktype == typeof(bool)) + { + fieldstr2.AppendLine($@" type: ""switch"""); + } + else if (checktype.IsEnum()) + { + fieldstr2.AppendLine($@" type: ""select"", + children: {item.FieldName}Types, + props: {{ + clearable: true, + placeholder: this.$t(""form.all"") + }}"); + + enums.Add(item.FieldName + "Types"); + } + else if (checktype.IsNumber()) + { + fieldstr2.AppendLine($@" type: ""input"""); + } + else if (checktype == typeof(string)) + { + fieldstr2.AppendLine($@" type: ""input"""); + } + else if (checktype == typeof(DateTime)) + { + fieldstr2.AppendLine($@" type: ""datePicker"", + span: 12, + props: {{ + type: ""datetimerange"", + ""value-format"": ""yyyy-MM-dd HH:mm:ss"", + ""range-separator"": ""-"", + ""start-placeholder"": this.$t(""table.startdate""), + ""end-placeholder"": this.$t(""table.enddate"") + }}"); + } + } + if (searchcount > 2) + { + fieldstr2.AppendLine(" ,isHidden: !this.isActive"); + } + fieldstr2.Append(" },"); + fieldstr2.Append(Environment.NewLine); + } + } + + string a1 = ""; + string a2 = ""; + foreach (var item in acts.Distinct()) + { + a1 += $@" @Action + {item}; + @State + {item}Data; +"; + a2 += $@" this.{item}(); +"; + } + + + return rv.Replace("$fields$", fieldstr2.ToString()).Replace("$actions$", actions.ToString()).Replace("$enums$", enums.Distinct().ToSepratedString()) + .Replace("$acts$", a1).Replace("$runactions$", a2); + + } + if (name == "store.api") + { + StringBuilder fieldstr = new StringBuilder(); + StringBuilder efieldstr = new StringBuilder(); + + var apis = apineeded.Distinct().ToList(); + for (int i = 0; i < apis.Count; i++) + { + var item = apis[i]; + fieldstr.AppendLine($@"const {item} = {{ + url: reqPath + ""{item}s"", + method: ""get"", + dataType: ""array"" +}}; "); + efieldstr.AppendLine($"{item},"); + } + return rv.Replace("$fields$", fieldstr.ToString()).Replace("$efields$", efieldstr.ToString()); + } + + return rv; + } + + public string GenerateBlazorView(string name) + { + string pagepath = string.IsNullOrEmpty(Area) ? $"/{ModelName}" : $"/{Area}/{ModelName}"; + if(name != "Index") + { + pagepath += $"/{name}"; + } + if (name == "Edit" || name == "Details") + { + pagepath += "/{id}"; + } + var rv = GetResource($"{name}.txt", "Spa.Blazor") + .Replace("$modelname$", ModelName) + .Replace("$vmnamespace$", VMNs) + .Replace("$des$", ModuleName) + .Replace("$controllername$", $"{ControllerNs},{ModelName}") + .Replace("$pagepath$", pagepath); + Type modelType = Type.GetType(SelectedModel); + if (name == "Index") + { + StringBuilder fieldstr = new StringBuilder(); + StringBuilder fieldstr2 = new StringBuilder(); + var pros = FieldInfos.Where(x => x.IsListField == true).ToList(); + var pros2 = FieldInfos.Where(x => x.IsSearcherField == true).ToList(); + List existSubPro = new List(); + Dictionary apis = new Dictionary(); + Dictionary multiapis = new Dictionary(); + for (int i = 0; i < pros.Count; i++) + { + var item = pros[i]; + var mpro = modelType.GetSingleProperty(item.FieldName); + string render = ""; + string template = ""; + string newname = item.FieldName; + if (mpro.PropertyType.IsBoolOrNullableBool()) + { + render = "ComponentType=\"@typeof(Switch)\""; + } + if (mpro.PropertyType == typeof(DateTime) || mpro.PropertyType == typeof(DateTime?)) + { + render = "FormatString=\"yyyy-MM-dd HH: mm: ss\""; + + } + if (string.IsNullOrEmpty(item.RelatedField) == false) + { + var subtype = Type.GetType(item.RelatedField); + string prefix = ""; + if (subtype == typeof(FileAttachment)) + { + if (item.FieldName.ToLower().Contains("photo") || item.FieldName.ToLower().Contains("pic") || item.FieldName.ToLower().Contains("icon") || item.FieldName.ToLower().Contains("zhaopian") || item.FieldName.ToLower().Contains("tupian")) + { + template = @" + "; + } + else + { + template = @" + "; + } + var fk = DC.GetFKName2(modelType, item.FieldName); + newname = fk; + } + else + { + var subpro = subtype.GetSingleProperty(item.SubField); + existSubPro.Add(subpro); + int count = existSubPro.Where(x => x.Name == subpro.Name).Count(); + if (count > 1) + { + prefix = count + ""; + } + newname = item.SubField + "_view" + prefix; + } + } + if (template == "") + { + fieldstr.Append($@" + "); + } + else + { + fieldstr.Append($@" + +{template} + "); + } + } + + for (int i = 0; i < pros2.Count; i++) + { + string controltype = "BootstrapInput"; + string sitems = ""; + string bindfield = ""; + string ph = ""; + var item = pros2[i]; + if (item.SubField == "`file") + { + continue; + } + + if (string.IsNullOrEmpty(item.RelatedField) == false) + { + if (string.IsNullOrEmpty(item.SubIdField) == true) + { + var fk = DC.GetFKName2(modelType, item.FieldName); + bindfield = fk; + } + else + { + bindfield = $"Selected{item.FieldName}IDs"; + } + } + else + { + bindfield = item.FieldName; + } + if (string.IsNullOrEmpty(item.RelatedField) == false) + { + var subtype = Type.GetType(item.RelatedField); + if (string.IsNullOrEmpty(item.SubIdField) == true) + { + controltype = "Select"; + } + else + { + controltype = "MultiSelect"; + } + var tempname = $"All{subtype.Name}s"; + sitems = $"Items=\"@{tempname}\""; + if (apis.ContainsKey(tempname) == false && multiapis.ContainsKey(tempname) == false) + { + if (controltype == "Select") + { + apis.Add(tempname, $"/api/{ModelName}/Get{subtype.Name}s"); + } + else + { + multiapis.Add(tempname, $"/api/{ModelName}/Get{subtype.Name}s"); + } + } + } + else + { + var proType = modelType.GetSingleProperty(item.FieldName)?.PropertyType; + Type checktype = proType; + if (proType.IsNullable()) + { + checktype = proType.GetGenericArguments()[0]; + } + if (checktype == typeof(bool)) + { + controltype = "Select"; + sitems = "Items=\"@WtmBlazor.GlobalSelectItems.SearcherBoolItems\""; + } + else if (checktype.IsEnum()) + { + controltype = "Select"; + } + else if (checktype.IsNumber()) + { + controltype = "BootstrapInputNumber"; + } + else if (checktype == typeof(string)) + { + } + else if (checktype == typeof(DateTime)) + { + controltype = "WTDateRange"; + } + } + if(controltype == "Select" || controltype == "MultiSelect") + { + ph = "PlaceHolder=\"@WtmBlazor.Localizer[\"Sys.All\"]\""; + } + fieldstr2.Append($@" + <{controltype} @bind-Value=""@SearchModel.{bindfield}"" {sitems} {ph}/>"); + } + + StringBuilder apiinit = new StringBuilder(); + StringBuilder fieldinit = new StringBuilder(); + foreach (var item in apis) + { + apiinit.Append(@$" + {item.Key} = await WtmBlazor.Api.CallItemsApi(""{item.Value}"", placeholder: WtmBlazor.Localizer[""Sys.All""]); +"); + fieldinit.Append($@" + private List {item.Key} = new List(); +"); + } + foreach (var item in multiapis) + { + apiinit.Append(@$" + {item.Key} = await WtmBlazor.Api.CallItemsApi(""{item.Value}""); +"); + fieldinit.Append($@" + private List {item.Key} = new List(); +"); + } + + return rv.Replace("$columns$", fieldstr.ToString()).Replace("$searchfields$",fieldstr2.ToString()).Replace("$init$", apiinit.ToString()).Replace("$fieldinit$", fieldinit.ToString()); + } + + + if (name == "Create" || name == "Edit") + { + StringBuilder fieldstr = new StringBuilder(); + var pros = FieldInfos.Where(x => x.IsFormField == true).ToList(); + + //生成表单model + Dictionary apis = new Dictionary(); + Dictionary multiapis = new Dictionary(); + for (int i = 0; i < pros.Count; i++) + { + var item = pros[i]; + string controltype = "BootstrapInput"; + string sitems = ""; + string bindfield = ""; + string ph = ""; + var property = modelType.GetSingleProperty(item.FieldName); + + if (string.IsNullOrEmpty(item.RelatedField) == false) + { + if (string.IsNullOrEmpty(item.SubIdField) == true) + { + var fk = DC.GetFKName2(modelType, item.FieldName); + bindfield = "Entity." + fk; + } + else + { + bindfield = $"Selected{item.FieldName}IDs"; + } + } + else + { + bindfield = "Entity."+ item.FieldName; + } + + if (string.IsNullOrEmpty(item.RelatedField) == false) + { + var subtype = Type.GetType(item.RelatedField); + if (item.SubField == "`file") + { + if (item.FieldName.ToLower().Contains("photo") || item.FieldName.ToLower().Contains("pic") || item.FieldName.ToLower().Contains("icon") || item.FieldName.ToLower().Contains("zhaopian") || item.FieldName.ToLower().Contains("tupian")) + { + controltype = "WTUploadImage"; + } + else + { + controltype = "WTUploadFile"; + } + } + else + { + if (string.IsNullOrEmpty(item.SubIdField) == true) + { + controltype = "Select"; + } + else + { + controltype = "Transfer"; + } + var tempname = $"All{subtype.Name}s"; + sitems = $"Items=\"@{tempname}\""; + if (apis.ContainsKey(tempname) == false && multiapis.ContainsKey(tempname) == false) + { + if (controltype == "Select") + { + apis.Add(tempname, $"/api/{ModelName}/Get{subtype.Name}s"); + } + else + { + multiapis.Add(tempname, $"/api/{ModelName}/Get{subtype.Name}s"); + } + } + + } + } + else + { + var proType = modelType.GetSingleProperty(item.FieldName)?.PropertyType; + Type checktype = proType; + if (proType.IsNullable()) + { + checktype = proType.GetGenericArguments()[0]; + } + if (checktype == typeof(bool)) + { + controltype = "Switch"; + } + else if (checktype.IsEnum()) + { + controltype = "Select"; + + } + else if (checktype.IsNumber()) + { + controltype = "BootstrapInputNumber"; + } + else if (checktype == typeof(string)) + { + } + else if (checktype == typeof(DateTime)) + { + controltype = "DateTimePicker"; + } + } + if (controltype == "Select" || controltype == "MultiSelect") + { + ph = "PlaceHolder=\"@WtmBlazor.Localizer[\"Sys.PleaseSelect\"]\""; + } + if (controltype == "Transfer") + { + fieldstr.Append($@" + + <{controltype} @bind-Value=""@Model.{bindfield}"" {sitems} {ph}/> + "); + } + else + { + fieldstr.Append($@" + <{controltype} @bind-Value=""@Model.{bindfield}"" {sitems} {ph}/>"); + } + } + + StringBuilder apiinit = new StringBuilder(); + StringBuilder fieldinit = new StringBuilder(); + foreach (var item in apis) + { + apiinit.Append(@$" + {item.Key} = await WtmBlazor.Api.CallItemsApi(""{item.Value}"", placeholder: WtmBlazor.Localizer[""Sys.PleaseSelect""]); +"); + fieldinit.Append($@" + private List {item.Key} = new List(); +"); + } + foreach (var item in multiapis) + { + apiinit.Append(@$" + {item.Key} = await WtmBlazor.Api.CallItemsApi(""{item.Value}""); +"); + fieldinit.Append($@" + private List {item.Key} = new List(); +"); + } + return rv.Replace("$formfields$", fieldstr.ToString()).Replace("$fieldinit$", fieldinit.ToString()).Replace("$init$", apiinit.ToString()); + } + if (name == "Details") + { + StringBuilder fieldstr = new StringBuilder(); + var pros = FieldInfos.Where(x => x.IsFormField == true).ToList(); + + //生成表单model + Dictionary apis = new Dictionary(); + for (int i = 0; i < pros.Count; i++) + { + var item = pros[i]; + string controltype = "Display"; + string sitems = ""; + string bindfield = ""; + string disabled = ""; + var property = modelType.GetSingleProperty(item.FieldName); + + if (string.IsNullOrEmpty(item.RelatedField) == false) + { + if (string.IsNullOrEmpty(item.SubIdField) == true) + { + var fk = DC.GetFKName2(modelType, item.FieldName); + bindfield = "Entity." + fk; + } + else + { + bindfield = $"Selected{item.FieldName}IDs"; + } + } + else + { + bindfield = "Entity." + item.FieldName; + } + if (string.IsNullOrEmpty(item.RelatedField) == false) + { + var subtype = Type.GetType(item.RelatedField); + if (item.SubField == "`file") + { + if (item.FieldName.ToLower().Contains("photo") || item.FieldName.ToLower().Contains("pic") || item.FieldName.ToLower().Contains("icon") || item.FieldName.ToLower().Contains("zhaopian") || item.FieldName.ToLower().Contains("tupian")) + { + controltype = "WTUploadImage"; + } + else + { + controltype = "WTUploadFile"; + } + disabled = "IsDisabled=\"true\""; + } + else + { + var tempname = $"All{subtype.Name}s"; + sitems = $"Lookup=\"@{tempname}\""; + if (apis.ContainsKey(tempname) == false) + { + apis.Add(tempname, $"/api/{ModelName}/Get{subtype.Name}s"); + } + } + } + else + { + var proType = modelType.GetSingleProperty(item.FieldName)?.PropertyType; + Type checktype = proType; + if (proType.IsNullable()) + { + checktype = proType.GetGenericArguments()[0]; + } + if (checktype == typeof(bool)) + { + controltype = "Switch"; + disabled = "IsDisabled=\"true\""; + } + } + if (controltype == "WTUploadFile") + { + string label = property.GetPropertyDisplayName(); + fieldstr.Append($@" + @if (Model.{bindfield}.HasValue){{ +
+ +
+
+ }} +"); + } + else + { + fieldstr.Append($@" + <{controltype} @bind-Value=""@Model.{bindfield}"" {sitems} {disabled} ShowLabel=""true""/>"); + } + } + + StringBuilder apiinit = new StringBuilder(); + StringBuilder fieldinit = new StringBuilder(); + foreach (var item in apis) + { + apiinit.Append(@$" + {item.Key} = await WtmBlazor.Api.CallItemsApi(""{item.Value}"", placeholder: WtmBlazor.Localizer[""Sys.All""]); +"); + fieldinit.Append($@" + private List {item.Key} = new List(); +"); + } + + return rv.Replace("$formfields$", fieldstr.ToString()).Replace("$fieldinit$", fieldinit.ToString()).Replace("$init$", apiinit.ToString()); + } + + + return rv; + } + + public string GetResource(string fileName, string subdir = "") + { + //获取编译在程序中的Controller原始代码文本 + Assembly assembly = Assembly.GetExecutingAssembly(); + string loc = ""; + if (string.IsNullOrEmpty(subdir)) + { + loc = $"WalkingTec.Mvvm.Mvc.GeneratorFiles.{fileName}"; + } + else + { + loc = $"WalkingTec.Mvvm.Mvc.GeneratorFiles.{subdir}.{fileName}"; + } + var textStreamReader = new StreamReader(assembly.GetManifestResourceStream(loc)); + string content = textStreamReader.ReadToEnd(); + textStreamReader.Close(); + return content; + } + + private string GetRelatedNamespace(List pros, string s) + { + string otherns = @""; + Type modelType = Type.GetType(SelectedModel); + foreach (var pro in pros) + { + Type proType = null; + + if (string.IsNullOrEmpty(pro.RelatedField)) + { + proType = modelType.GetSingleProperty(pro.FieldName)?.PropertyType; + } + else + { + proType = Type.GetType(pro.RelatedField); + } + string prons = proType.Namespace; + if (proType.IsNullable()) + { + prons = proType.GetGenericArguments()[0].Namespace; + } + if (s.Contains($"using {prons};") == false && otherns.Contains($"using {prons};") == false) + { + otherns += $@"using {prons}; +"; + } + + } + + return s.Replace("$othernamespace$", otherns); + } + + } + + public enum FieldInfoType { Normal, One2Many, Many2Many } + + public class FieldInfo + { + public string FieldName { get; set; } + public string RelatedField { get; set; } + + public bool IsSearcherField { get; set; } + + public bool IsListField { get; set; } + + public bool IsFormField { get; set; } + + public bool IsImportField { get; set; } + public bool IsBatchField { get; set; } + + public FieldInfoType InfoType + { + get + { + if (string.IsNullOrEmpty(RelatedField)) + { + return FieldInfoType.Normal; + } + else + { + if (string.IsNullOrEmpty(SubIdField)) + { + return FieldInfoType.One2Many; + } + else + { + return FieldInfoType.Many2Many; + } + } + } + } + + /// + /// 字段关联的类名 + /// + public string SubField { get; set; } + /// + /// 多对多关系时,记录中间表关联到主表的字段名称 + /// + public string SubIdField { get; set; } + + public string GetField(IDataContext DC, Type modelType) + { + if (this.InfoType == FieldInfoType.One2Many) + { + var fk = DC.GetFKName2(modelType, this.FieldName); + return fk; + } + else + { + return this.FieldName; + } + } + + public string GetFKType(IDataContext DC, Type modelType) + { + Type fktype = null; + if (this.InfoType == FieldInfoType.One2Many) + { + var fk = this.GetField(DC, modelType); + fktype = modelType.GetSingleProperty(fk)?.PropertyType; + } + if (this.InfoType == FieldInfoType.Many2Many) + { + var middletype = modelType.GetSingleProperty(this.FieldName)?.PropertyType; + fktype = middletype.GetGenericArguments()[0].GetSingleProperty(this.SubIdField)?.PropertyType; + } + var typename = "string"; + + if (fktype == typeof(short) || fktype == typeof(short?)) + { + typename = "short"; + } + if (fktype == typeof(int) || fktype == typeof(int?)) + { + typename = "int"; + } + if (fktype == typeof(long) || fktype == typeof(long?)) + { + typename = "long"; + } + if (fktype == typeof(Guid) || fktype == typeof(Guid?)) + { + typename = "Guid"; + } + + return typename; + + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Filters/DataContextFilter.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Filters/DataContextFilter.cs new file mode 100644 index 0000000..e292c7a --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Filters/DataContextFilter.cs @@ -0,0 +1,78 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using WalkingTec.Mvvm.Core; + +namespace WalkingTec.Mvvm.Mvc.Filters +{ + public class DataContextFilter : ActionFilterAttribute + { + public static Func _csfunc; + + public DataContextFilter() + { + + } + + public override void OnActionExecuting(ActionExecutingContext context) + { + var controller = context.Controller as IBaseController; + if (controller == null) + { + base.OnActionExecuting(context); + return; + } + context.SetWtmContext(); + string cs = ""; + DBTypeEnum? dbtype = null; + ControllerActionDescriptor ad = context.ActionDescriptor as ControllerActionDescriptor; + var fixcontroller = ad.ControllerTypeInfo.GetCustomAttributes(typeof(FixConnectionAttribute), false).Cast().FirstOrDefault(); + var fixaction = ad.MethodInfo.GetCustomAttributes(typeof(FixConnectionAttribute), false).Cast().FirstOrDefault(); + var ispost = ad.MethodInfo.GetCustomAttributes(typeof(HttpPostAttribute), false).Cast().FirstOrDefault(); + string mode = "Read"; + + if (fixcontroller != null || fixaction != null) + { + cs = fixaction?.CsName ?? fixcontroller?.CsName; + var op = fixcontroller?.Operation ?? fixaction?.Operation; + if (op != null) + { + switch (op.Value) + { + case DBOperationEnum.Read: + mode = "Read"; + break; + case DBOperationEnum.Write: + mode = "Write"; + break; + default: + break; + } + } + dbtype = fixcontroller?.DbType ?? fixaction?.DbType; + cs = Utils.GetCS(cs, mode, controller.Wtm.ConfigInfo); + } + else + { + cs = _csfunc?.Invoke(context); + if (string.IsNullOrEmpty(cs)) + { + if(ispost != null) + { + mode = "Write"; + } + cs = context.HttpContext.Request.Query["DONOTUSECSName"]; + cs = Utils.GetCS(cs, mode, controller.Wtm.ConfigInfo); + } + } + + controller.Wtm.CurrentCS = cs; + controller.Wtm.CurrentDbType = dbtype; + base.OnActionExecuting(context); + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Filters/FrameworkFilter.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Filters/FrameworkFilter.cs new file mode 100644 index 0000000..d85688a --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Filters/FrameworkFilter.cs @@ -0,0 +1,433 @@ +using System; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; +using WalkingTec.Mvvm.Core; +using System.Collections.Generic; +using System.Reflection; +using WalkingTec.Mvvm.Core.Implement; +using System.IO; +using System.Text.RegularExpressions; +using System.Text; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using WalkingTec.Mvvm.Core.Support.Json; +using System.Text.Json; +using WalkingTec.Mvvm.Core.Json; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Mvc.ViewEngines; + +namespace WalkingTec.Mvvm.Mvc.Filters +{ + public class FrameworkFilter : ActionFilterAttribute + { + public override void OnActionExecuting(ActionExecutingContext context) + { + var ctrl = context.Controller as IBaseController; + if (ctrl == null) + { + base.OnActionExecuting(context); + return; + } + context.SetWtmContext(); + if (context.HttpContext.Items.ContainsKey("actionstarttime") == false) + { + context.HttpContext.Items.Add("actionstarttime", DateTime.Now); + } + var ctrlActDesc = context.ActionDescriptor as ControllerActionDescriptor; + var log = new SimpleLog();// 初始化log备用 + var ctrlDes = ctrlActDesc.ControllerTypeInfo.GetCustomAttributes(typeof(ActionDescriptionAttribute), false).Cast().FirstOrDefault(); + var actDes = ctrlActDesc.MethodInfo.GetCustomAttributes(typeof(ActionDescriptionAttribute), false).Cast().FirstOrDefault(); + var postDes = ctrlActDesc.MethodInfo.GetCustomAttributes(typeof(HttpPostAttribute), false).Cast().FirstOrDefault(); + var validpostonly = ctrlActDesc.MethodInfo.GetCustomAttributes(typeof(ValidateFormItemOnlyAttribute), false).Cast().FirstOrDefault(); + + log.ITCode = ctrl.Wtm.LoginUserInfo?.ITCode ?? string.Empty; + //给日志的多语言属性赋值 + log.ModuleName = ctrlDes?.GetDescription(ctrl) ?? ctrlActDesc.ControllerName; + log.ActionName = actDes?.GetDescription(ctrl) ?? ctrlActDesc.ActionName + (postDes == null ? string.Empty : "[P]"); + log.ActionUrl = ctrl.BaseUrl; + log.IP = context.HttpContext.Connection.RemoteIpAddress.ToString(); + + ctrl.Wtm.Log = log; + foreach (var item in context.ActionArguments) + { + if (item.Value is BaseVM) + { + var model = item.Value as BaseVM; + model.Wtm = ctrl.Wtm; + model.FC = new Dictionary(); + model.CreatorAssembly = this.GetType().Assembly.FullName; + model.ControllerName = context.HttpContext.Request.Path; + //if (ctrl is BaseController c) + //{ + // model.WtmContext.WindowIds = c.WindowIds; + // model.UIService = c.UIService; + //} + //else + //{ + // model.WindowIds = ""; + // model.UIService = new DefaultUIService(); + //} + try + { + var f = context.HttpContext.Request.Form; + foreach (var key in f.Keys) + { + if (model.FC.Keys.Contains(key) == false) + { + model.FC.Add(key, f[key]); + } + } + if (context.HttpContext.Request.QueryString != QueryString.Empty) + { + foreach (var key in context.HttpContext.Request.Query.Keys) + { + if (model.FC.Keys.Contains(key) == false) + { + model.FC.Add(key, context.HttpContext.Request.Query[key]); + } + } + } + } + catch { } + + if (ctrl is BaseApiController apictrl) + { + //apictrl.TryValidateModel(model); + if (context.HttpContext.Items.ContainsKey("DONOTUSE_REQUESTBODY")) + { + string body = context.HttpContext.Items["DONOTUSE_REQUESTBODY"].ToString(); + var joption = new JsonSerializerOptions(); + joption.Converters.Add(new BodyConverter()); + try + { + var obj = JsonSerializer.Deserialize(body, joption); + foreach (var field in obj.ProNames) + { + + model.FC.Add(field, ""); + } + } + catch { } + } + } + //if (model is IBaseCRUDVM crud) + //{ + // var pros = crud.Entity.GetType().GetProperties(); + // foreach (var pro in pros) + // { + // if (model.FC.ContainsKey("Entity." + pro.Name)) + // { + // //找到类型为List的字段 + // if (pro.PropertyType.GenericTypeArguments.Count() > 0) + // { + // //获取xxx的类型 + // var ftype = pro.PropertyType.GenericTypeArguments.First(); + // //如果xxx继承自TopBasePoco + // if (ftype.IsSubclassOf(typeof(TopBasePoco))) + // { + // //界面传过来的子表数据 + + // if (pro.GetValue(crud.Entity) is IEnumerable list && list.Count() == 0) + // { + // pro.SetValue(crud.Entity, null); + // } + // } + // } + // } + // } + //} + //如果ViewModel T继承自IBaseBatchVM,则自动为其中的ListVM和EditModel初始化数据 + if (model is IBaseBatchVM temp) + { + if (temp.ListVM != null) + { + temp.ListVM.CopyContext(model); + temp.ListVM.Ids = temp.Ids == null ? new List() : temp.Ids.ToList(); + temp.ListVM.SearcherMode = ListVMSearchModeEnum.Batch; + temp.ListVM.NeedPage = false; + } + if (temp.LinkedVM != null) + { + temp.LinkedVM.CopyContext(model); + } + if (temp.ListVM != null) + { + //绑定ListVM的OnAfterInitList事件,当ListVM的InitList完成时,自动将操作列移除 + temp.ListVM.OnAfterInitList += (self) => + { + self.RemoveActionColumn(); + self.RemoveAction(); + self.AddErrorColumn(); + }; + if (temp.ListVM.Searcher != null) + { + var searcher = temp.ListVM.Searcher; + searcher.CopyContext(model); + } + temp.ListVM.DoInitListVM(); + } + temp.LinkedVM?.DoInit(); + } + if (model is IBaseImport tvm) + { + var template = tvm.Template; + template.CopyContext(model); + template.DoReInit(); + var errorlist = tvm.ErrorListVM; + errorlist.CopyContext(model); + } + if (model is IBasePagedListVM lvm) + { + var searcher = lvm.Searcher; + searcher.CopyContext(lvm); + if (ctrl is BaseController) + { + searcher.DoInit(); + } + } + model.Validate(); + var invalid = ctrl.ModelState.Where(x => x.Value.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid).Select(x => x.Key).ToList(); + if ((ctrl as ControllerBase).Request.Method.ToLower() == "put" || validpostonly != null) + { + foreach (var v in invalid) + { + if (v?.StartsWith("Entity.") == true) + { + Regex r = new Regex("(.*?)\\[.*?\\](.*?$)"); + var m = r.Match(v); + var check = v; + if (m.Success) + { + check = m.Groups[1] + "[0]" + m.Groups[2]; + } + if (model.FC.ContainsKey(check) == false) + { + ctrl.ModelState.Remove(v); + } + } + } + } + if (ctrl is BaseController) + { + var reinit = model.GetType().GetTypeInfo().GetCustomAttributes(typeof(ReInitAttribute), false).Cast().SingleOrDefault(); + if (ctrl.ModelState.IsValid) + { + if (reinit != null && (reinit.ReInitMode == ReInitModes.SUCCESSONLY || reinit.ReInitMode == ReInitModes.ALWAYS)) + { + model.DoReInit(); + } + } + else + { + if (reinit == null || (reinit.ReInitMode == ReInitModes.FAILEDONLY || reinit.ReInitMode == ReInitModes.ALWAYS)) + { + model.DoReInit(); + } + } + } + + //如果是子表外键验证错误,例如Entity.Majors[0].SchoolId为空这种错误,则忽略。因为框架会在添加修改的时候自动给外键赋值 + var toremove = ctrl.ModelState.Select(x => x.Key).Where(x => Regex.IsMatch(x, ".*?\\[.*?\\]\\..*?id", RegexOptions.IgnoreCase)); + foreach (var r in toremove) + { + ctrl.ModelState.Remove(r); + } + } + + if(item.Value is BaseSearcher se) + { + se.FC = new Dictionary(); + se.Wtm = ctrl.Wtm; + se.Validate(); + } + } + + //忽略绑定List由于前台传递空字符串造成的错误 + base.OnActionExecuting(context); + } + + + + public override void OnActionExecuted(ActionExecutedContext context) + { + var ctrl = context.Controller as BaseController; + if (ctrl == null) + { + base.OnActionExecuted(context); + return; + } + ctrl.ViewData["DONOTUSE_COOKIEPRE"] = ctrl.Wtm.ConfigInfo.CookiePre; + var ctrlActDesc = context.ActionDescriptor as ControllerActionDescriptor; + //get viewname + string viewName = ""; + if(context.Result is PartialViewResult pvr) + { + viewName = pvr.ViewName??""; + if (viewName?.StartsWith("/") == false) + { + var viewEngine = context.HttpContext.RequestServices.GetRequiredService(); + viewName = viewEngine.FindView(context, string.IsNullOrEmpty(viewName) ? ctrlActDesc.ActionName : viewName, false)?.View?.Path; + } + } + if (context.Result is ViewResult vr) + { + viewName = vr.ViewName??""; + if (viewName?.StartsWith("/") == false) + { + var viewEngine = context.HttpContext.RequestServices.GetRequiredService(); + viewName = viewEngine.FindView(context, string.IsNullOrEmpty(viewName) ? ctrlActDesc.ActionName : viewName, false)?.View?.Path; + } + } + if (context.Result is PartialViewResult) + { + var model = (context.Result as PartialViewResult).ViewData?.Model as BaseVM; + if (model == null && (context.Result as PartialViewResult).Model == null && (context.Result as PartialViewResult).ViewData != null) + { + model = ctrl.Wtm.CreateVM(); + model.CurrentView = viewName; + (context.Result as PartialViewResult).ViewData.Model = model; + } + // 为所有 PartialView 加上最外层的 Div + if (model != null) + { + model.CurrentView = viewName; + string pagetitle = string.Empty; + var menu = Utils.FindMenu(context.HttpContext.Request.Path,ctrl.GlobaInfo.AllMenus); + if (menu == null) + { + var ctrlDes = ctrlActDesc.ControllerTypeInfo.GetCustomAttributes(typeof(ActionDescriptionAttribute), false).Cast().FirstOrDefault(); + var actDes = ctrlActDesc.MethodInfo.GetCustomAttributes(typeof(ActionDescriptionAttribute), false).Cast().FirstOrDefault(); + if (actDes != null) + { + if (ctrlDes != null) + { + pagetitle = ctrlDes.GetDescription(ctrl) + " - "; + } + pagetitle += actDes.GetDescription(ctrl); + } + } + else + { + if (menu.ParentId != null) + { + var pmenu = ctrl.GlobaInfo.AllMenus.Where(x => x.ID == menu.ParentId).FirstOrDefault(); + if (pmenu != null) + { + pmenu.PageName = Core.CoreProgram._localizer?[pmenu.PageName]; + + pagetitle = pmenu.PageName + " - "; + } + } + menu.PageName = Core.CoreProgram._localizer?[menu.PageName]; + pagetitle += menu.PageName; + } + if (string.IsNullOrEmpty(pagetitle) == false) + { + context.HttpContext.Response.Headers.Add("X-wtm-PageTitle", Convert.ToBase64String(Encoding.UTF8.GetBytes(pagetitle))); + } + context.HttpContext.Response.Cookies.Append("divid", model.ViewDivId); + } + } + if (context.Result is ViewResult) + { + var model = (context.Result as ViewResult).ViewData?.Model as BaseVM; + if (model == null && (context.Result as ViewResult).Model == null && (context.Result as ViewResult).ViewData != null) + { + model = ctrl.Wtm.CreateVM(); + model.CurrentView = viewName; + (context.Result as ViewResult).ViewData.Model = model; + } + if (model != null) + { + model.CurrentView = viewName; + context.HttpContext.Response.Cookies.Append("divid", model?.ViewDivId); + } + } + base.OnActionExecuted(context); + } + + public override void OnResultExecuted(ResultExecutedContext context) + { + var ctrl = context.Controller as IBaseController; + if (ctrl == null) + { + base.OnResultExecuted(context); + return; + } + var ctrlActDesc = context.ActionDescriptor as ControllerActionDescriptor; + var nolog = ctrlActDesc.MethodInfo.IsDefined(typeof(NoLogAttribute), false) || ctrlActDesc.ControllerTypeInfo.IsDefined(typeof(NoLogAttribute), false); + + BaseVM model = null; + if (context.Result is ViewResult vr) + { + model = vr.Model as BaseVM; + } + if (context.Result is PartialViewResult pvr) + { + model = pvr.Model as BaseVM; + context.HttpContext.Response.WriteAsync($""); + } + + //如果是来自Error,则已经记录过日志,跳过 + if (ctrlActDesc.ControllerName == "_Framework" && ctrlActDesc.ActionName == "Error") + { + return; + } + if ( nolog == false) + { + var log = new ActionLog(); + var ctrlDes = ctrlActDesc.ControllerTypeInfo.GetCustomAttributes(typeof(ActionDescriptionAttribute), false).Cast().FirstOrDefault(); + var actDes = ctrlActDesc.MethodInfo.GetCustomAttributes(typeof(ActionDescriptionAttribute), false).Cast().FirstOrDefault(); + var postDes = ctrlActDesc.MethodInfo.GetCustomAttributes(typeof(HttpPostAttribute), false).Cast().FirstOrDefault(); + + log.LogType = context.Exception == null ? ActionLogTypesEnum.Normal : ActionLogTypesEnum.Exception; + log.ActionTime = DateTime.Now; + log.ITCode = ctrl.Wtm?.LoginUserInfo?.ITCode ?? string.Empty; + // 给日志的多语言属性赋值 + log.ModuleName = ctrlDes?.GetDescription(ctrl) ?? ctrlActDesc.ControllerName; + log.ActionName = actDes?.GetDescription(ctrl) ?? ctrlActDesc.ActionName + (postDes == null ? string.Empty : "[P]"); + log.ActionUrl = context.HttpContext.Request.Path; + log.IP = context.HttpContext.GetRemoteIpAddress(); + log.Remark = context.Exception?.ToString() ?? string.Empty; + if (string.IsNullOrEmpty(log.Remark) == false && log.Remark.Length > 2000) + { + log.Remark = log.Remark.Substring(0, 2000); + } + var starttime = context.HttpContext.Items["actionstarttime"] as DateTime?; + if (starttime != null) + { + log.Duration = DateTime.Now.Subtract(starttime.Value).TotalSeconds; + } + try + { + var logger = context.HttpContext.RequestServices.GetRequiredService>(); + if (logger != null) + { + logger.Log(LogLevel.Information, new EventId(), log, null, (a, b) => + { + return a.GetLogString(); + }); + } + } + catch { } + } + if (context.Exception != null) + { + context.ExceptionHandled = true; + if (ctrl.Wtm.ConfigInfo.IsQuickDebug == true) + { + context.HttpContext.Response.WriteAsync(context.Exception.ToString()); + } + else + { + context.HttpContext.Response.WriteAsync(MvcProgram._localizer["Sys.PageError"]); + } + } + base.OnResultExecuted(context); + } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Filters/PrivilegeFilter.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Filters/PrivilegeFilter.cs new file mode 100644 index 0000000..d3e1507 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Filters/PrivilegeFilter.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Policy; +using System.Text.RegularExpressions; +using System.Web; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using WalkingTec.Mvvm.Core; +using WalkingTec.Mvvm.Core.Auth; + +namespace WalkingTec.Mvvm.Mvc.Filters +{ + public class PrivilegeFilter : ActionFilterAttribute + { + public override void OnActionExecuting(ActionExecutingContext context) + { + var controller = context.Controller as IBaseController; + if (controller == null) + { + base.OnActionExecuting(context); + return; + } + context.SetWtmContext(); + + if (controller.Wtm.ConfigInfo.IsQuickDebug && controller is BaseApiController) + { + base.OnActionExecuting(context); + return; + } + ControllerActionDescriptor ad = context.ActionDescriptor as ControllerActionDescriptor; + + var lg = context.HttpContext.RequestServices.GetRequiredService(); + + string u = null; + if (ad.Parameters.Any(x=>x.Name.ToLower() == "id")) + { + u = lg.GetPathByAction(ad.ActionName, ad.ControllerName, new { area = context.RouteData.Values["area"], id = 0 }); + } + else + { + u = lg.GetPathByAction(ad.ActionName, ad.ControllerName, new { area = context.RouteData.Values["area"] }); + } + if (u != null && u.EndsWith("/0")) + { + u = u[0..^2]; + if (controller is BaseApiController) + { + u = u + "/{id}"; + } + } + if (u != null && u.EndsWith("?id=0")) + { + u = u[0..^5]; + if (controller is BaseApiController) + { + u = u + "/{id}"; + } + } + + controller.Wtm.BaseUrl = u + context.HttpContext.Request.QueryString.ToUriComponent(); + + + //如果是QuickDebug模式,或者Action或Controller上有AllRightsAttribute标记都不需要判断权限 + //如果用户登录信息为空,也不需要判断权限,BaseController中会对没有登录的用户做其他处理 + + var isPublic = ad.MethodInfo.IsDefined(typeof(PublicAttribute), false) || ad.ControllerTypeInfo.IsDefined(typeof(PublicAttribute), false); + if (!isPublic) + isPublic = ad.MethodInfo.IsDefined(typeof(AllowAnonymousAttribute), false) || ad.ControllerTypeInfo.IsDefined(typeof(AllowAnonymousAttribute), false); + + var isAllRights = ad.MethodInfo.IsDefined(typeof(AllRightsAttribute), false) || ad.ControllerTypeInfo.IsDefined(typeof(AllRightsAttribute), false); + var isDebug = ad.MethodInfo.IsDefined(typeof(DebugOnlyAttribute), false) || ad.ControllerTypeInfo.IsDefined(typeof(DebugOnlyAttribute), false); + if (controller.Wtm.ConfigInfo.IsFilePublic == true) + { + if (ad.ControllerName == "_Framework" && (ad.MethodInfo.Name == "GetFile" || ad.MethodInfo.Name == "ViewFile")) + { + isPublic = true; + } + if(ad.ControllerTypeInfo.FullName == "WalkingTec.Mvvm.Admin.Api.FileApiController" && (ad.MethodInfo.Name == "GetFileName" || ad.MethodInfo.Name == "GetFile" || ad.MethodInfo.Name == "DownloadFile")) + { + isPublic = true; + + } + } + if (isDebug) + { + if (controller.ConfigInfo.IsQuickDebug) + { + base.OnActionExecuting(context); + } + else + { + if (controller is BaseController c) + { + context.Result = c.Content(MvcProgram._localizer["Sys.DebugOnly"]); + } + else if (controller is ControllerBase c2) + { + context.Result = c2.BadRequest(MvcProgram._localizer["Sys.DebugOnly"]); + } + } + return; + } + + if(isPublic == false && controller != null && controller.Wtm != null) + { + isPublic = controller.Wtm.IsUrlPublic(u); + } + if (isPublic == true) + { + base.OnActionExecuting(context); + return; + } + + if (controller.Wtm.LoginUserInfo == null) + { + if (controller is ControllerBase ctrl) + { + //if it's a layui search request,returns a layui format message so that it can parse + if (ctrl.Request.Headers.ContainsKey("layuisearch")) + { + ContentResult cr = new ContentResult() + { + Content = "{\"Data\":[],\"Count\":0,\"Page\":1,\"PageCount\":0,\"Msg\":\"" + MvcProgram._localizer["Sys.NeedLogin"] + "\",\"Code\":401}", + ContentType = "application/json", + StatusCode = 200 + }; + context.Result = cr; + } + else + { + if (ctrl.HttpContext.Request.Headers.ContainsKey("Authorization")) + { + context.Result = ctrl.Unauthorized(JwtBearerDefaults.AuthenticationScheme); + } + else + { + if (controller is BaseApiController) + { + ContentResult cr = new ContentResult() + { + Content = MvcProgram._localizer["Sys.NeedLogin"], + ContentType = "text/html", + StatusCode = 401 + }; + context.Result = cr; + } + else + { + string lp = controller.Wtm.ConfigInfo.CookieOptions.LoginPath; + if (lp.StartsWith("/")) + { + lp = "~" + lp; + } + if (lp.StartsWith("~/")) + { + lp = ctrl.Url.Content(lp); + } + ContentResult cr = new ContentResult() + { + Content = $"", + ContentType = "text/html", + StatusCode = 200 + }; + //context.HttpContext.Response.Headers.Add("IsScript", "true"); + context.Result = cr; + //context.Result = ctrl.Redirect(GlobalServices.GetRequiredService>().Value.LoginPath); + } + } + } + } + //context.HttpContext.ChallengeAsync().Wait(); + } + else + { + if (isAllRights == false) + { + bool canAccess = controller.Wtm.IsAccessable(controller.BaseUrl); + if (canAccess == false && controller.ConfigInfo.IsQuickDebug == false) + { + if (controller is ControllerBase ctrl) + { + //if it's a layui search request,returns a layui format message so that it can parse + if (ctrl.Request.Headers.ContainsKey("layuisearch")) + { + ContentResult cr = new ContentResult() + { + Content = "{\"Data\":[],\"Count\":0,\"Page\":1,\"PageCount\":0,\"Msg\":\""+ MvcProgram._localizer["Sys.NoPrivilege"] + "\",\"Code\":403}", + ContentType = "application/json", + StatusCode = 200 + }; + context.Result = cr; + } + else + { + if (ctrl.HttpContext.Request.Headers.ContainsKey("Authorization")) + { + context.Result = ctrl.Forbid(JwtBearerDefaults.AuthenticationScheme); + } + else + { + ContentResult cr = new ContentResult() + { + Content = MvcProgram._localizer["Sys.NoPrivilege"], + ContentType = "text/html", + StatusCode = 403 + }; + context.Result = cr; + + } + } + } + } + } + } + base.OnActionExecuting(context); + } + + private List getAuthTypes(ControllerActionDescriptor ad) + { + var authenticationSchemes = new List(); + if (ad.MethodInfo.IsDefined(typeof(AuthorizeAttribute), false)) + { + var authorizeAttr = ad.MethodInfo.GetCustomAttributes(typeof(AuthorizeAttribute), false).FirstOrDefault() as AuthorizeAttribute; + if (authorizeAttr != null) + authenticationSchemes = authorizeAttr.AuthenticationSchemes.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); + } + else if (ad.ControllerTypeInfo.IsDefined(typeof(AuthorizeAttribute), false)) + { + var authorizeAttr = ad.ControllerTypeInfo.GetCustomAttributes(typeof(AuthorizeAttribute), false).FirstOrDefault() as AuthorizeAttribute; + if (authorizeAttr != null) + authenticationSchemes = authorizeAttr.AuthenticationSchemes.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); + } + return authenticationSchemes; + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Filters/SwaggerFilter.cs b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Filters/SwaggerFilter.cs new file mode 100644 index 0000000..5a5750e --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/Filters/SwaggerFilter.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; +using WalkingTec.Mvvm.Core; + +namespace WalkingTec.Mvvm.Mvc.Filters +{ + public class SwaggerFilter : ISchemaFilter + { + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + var type = context.Type; + if(type == typeof(List)) + { + schema = null; + } + //if (type.IsEnum) + //{ + // schema.Extensions.Add( + // "x-ms-enum", + // new OpenApiObject + // { + // ["name"] = new OpenApiString(type.Name), + // ["modelAsString"] = new OpenApiBoolean(true) + // } + // ); + //}; + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/ApiTest.txt b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/ApiTest.txt new file mode 100644 index 0000000..851f251 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/ApiTest.txt @@ -0,0 +1,130 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using WalkingTec.Mvvm.Core; +using $cns$; +using $vns$; +using $mns$; +using $dns$; +$othernamespace$ + +namespace $tns$ +{ + [TestClass] + public class $model$ApiTest + { + private $classnamel$Controller _controller; + private string _seed; + + public $model$ApiTest() + { + _seed = Guid.NewGuid().ToString(); + _controller = MockController.CreateApi<$classnamel$Controller>(new DataContext(_seed, DBTypeEnum.Memory), "user"); + } + + [TestMethod] + public void SearchTest() + { + ContentResult rv = _controller.Search(new $classnamel$Searcher()) as ContentResult; + Assert.IsTrue(string.IsNullOrEmpty(rv.Content)==false); + } + + [TestMethod] + public void CreateTest() + { + $classnamel$VM vm = _controller.Wtm.CreateVM<$classnamel$VM>(); + $model$ v = new $model$(); + $cpros$ + vm.Entity = v; + var rv = _controller.Add(vm); + Assert.IsInstanceOfType(rv, typeof(OkObjectResult)); + + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + var data = context.Set<$model$>().Find(v.ID); + $assert$ + Assert.AreEqual(data.CreateBy, "user"); + Assert.IsTrue(DateTime.Now.Subtract(data.CreateTime.Value).Seconds < 10); + } + } + + [TestMethod] + public void EditTest() + { + $model$ v = new $model$(); + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + $pros$ + context.Set<$model$>().Add(v); + context.SaveChanges(); + } + + $classnamel$VM vm = _controller.Wtm.CreateVM<$classnamel$VM>(); + var oldID = v.ID; + v = new $model$(); + v.ID = oldID; + $epros$ + vm.Entity = v; + vm.FC = new Dictionary(); + $fc$ + var rv = _controller.Edit(vm); + Assert.IsInstanceOfType(rv, typeof(OkObjectResult)); + + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + var data = context.Set<$model$>().Find(v.ID); + $eassert$ + Assert.AreEqual(data.UpdateBy, "user"); + Assert.IsTrue(DateTime.Now.Subtract(data.UpdateTime.Value).Seconds < 10); + } + + } + + [TestMethod] + public void GetTest() + { + $model$ v = new $model$(); + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + $pros$ + context.Set<$model$>().Add(v); + context.SaveChanges(); + } + var rv = _controller.Get(v.ID.ToString()); + Assert.IsNotNull(rv); + } + + [TestMethod] + public void BatchDeleteTest() + { + $model$ v1 = new $model$(); + $model$ v2 = new $model$(); + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + $mpros$ + context.Set<$model$>().Add(v1); + context.Set<$model$>().Add(v2); + context.SaveChanges(); + } + + var rv = _controller.BatchDelete(new string[] { v1.ID.ToString(), v2.ID.ToString() }); + Assert.IsInstanceOfType(rv, typeof(OkObjectResult)); + + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + var data1 = context.Set<$model$>().Find(v1.ID); + var data2 = context.Set<$model$>().Find(v2.ID); + $mdel$ + } + + rv = _controller.BatchDelete(new string[] {}); + Assert.IsInstanceOfType(rv, typeof(OkResult)); + + } +$add$ + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/ApiTestTopPoco.txt b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/ApiTestTopPoco.txt new file mode 100644 index 0000000..d0ef3ee --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/ApiTestTopPoco.txt @@ -0,0 +1,126 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using WalkingTec.Mvvm.Core; +using $cns$; +using $vns$; +using $mns$; +using $dns$; +$othernamespace$ + +namespace $tns$ +{ + [TestClass] + public class $model$ApiTest + { + private $classnamel$Controller _controller; + private string _seed; + + public $model$ApiTest() + { + _seed = Guid.NewGuid().ToString(); + _controller = MockController.CreateApi<$classnamel$Controller>(new DataContext(_seed, DBTypeEnum.Memory), "user"); + } + + [TestMethod] + public void SearchTest() + { + ContentResult rv = _controller.Search(new $classnamel$Searcher()) as ContentResult; + Assert.IsTrue(string.IsNullOrEmpty(rv.Content)==false); + } + + [TestMethod] + public void CreateTest() + { + $classnamel$VM vm = _controller.Wtm.CreateVM<$classnamel$VM>(); + $model$ v = new $model$(); + $cpros$ + vm.Entity = v; + var rv = _controller.Add(vm); + Assert.IsInstanceOfType(rv, typeof(OkObjectResult)); + + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + var data = context.Set<$model$>().Find(v.ID); + $assert$ + } + } + + [TestMethod] + public void EditTest() + { + $model$ v = new $model$(); + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + $pros$ + context.Set<$model$>().Add(v); + context.SaveChanges(); + } + + $classnamel$VM vm = _controller.Wtm.CreateVM<$classnamel$VM>(); + var oldID = v.ID; + v = new $model$(); + v.ID = oldID; + $epros$ + vm.Entity = v; + vm.FC = new Dictionary(); + $fc$ + var rv = _controller.Edit(vm); + Assert.IsInstanceOfType(rv, typeof(OkObjectResult)); + + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + var data = context.Set<$model$>().Find(v.ID); + $eassert$ + } + + } + + [TestMethod] + public void GetTest() + { + $model$ v = new $model$(); + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + $pros$ + context.Set<$model$>().Add(v); + context.SaveChanges(); + } + var rv = _controller.Get(v.ID.ToString()); + Assert.IsNotNull(rv); + } + + [TestMethod] + public void BatchDeleteTest() + { + $model$ v1 = new $model$(); + $model$ v2 = new $model$(); + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + $mpros$ + context.Set<$model$>().Add(v1); + context.Set<$model$>().Add(v2); + context.SaveChanges(); + } + + var rv = _controller.BatchDelete(new string[] { v1.ID.ToString(), v2.ID.ToString() }); + Assert.IsInstanceOfType(rv, typeof(OkObjectResult)); + + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + var data1 = context.Set<$model$>().Find(v1.ID); + var data2 = context.Set<$model$>().Find(v2.ID); + $mdel$ + } + + rv = _controller.BatchDelete(new string[] {}); + Assert.IsInstanceOfType(rv, typeof(OkResult)); + + } +$add$ + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/BatchVM.txt b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/BatchVM.txt new file mode 100644 index 0000000..52cc6fa --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/BatchVM.txt @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using WalkingTec.Mvvm.Core; +using WalkingTec.Mvvm.Core.Extensions; +using $modelnamespace$; +$othernamespace$ + +namespace $vmnamespace$ +{ + public partial class $classname$BatchVM : BaseBatchVM<$modelname$, $classname$_BatchEdit> + { + public $classname$BatchVM() + { + ListVM = new $classname$ListVM(); + LinkedVM = new $classname$_BatchEdit(); + } + + } + + /// + /// Class to define batch edit fields + /// + public class $classname$_BatchEdit : BaseVM + {$pros$ + + protected override void InitVM() + {$init$ + } + + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/ControllerTest.txt b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/ControllerTest.txt new file mode 100644 index 0000000..d4c7990 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/ControllerTest.txt @@ -0,0 +1,213 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using WalkingTec.Mvvm.Core; +using $cns$; +using $vns$; +using $mns$; +using $dns$; +$othernamespace$ + +namespace $tns$ +{ + [TestClass] + public class $model$ControllerTest + { + private $model$Controller _controller; + private string _seed; + + public $model$ControllerTest() + { + _seed = Guid.NewGuid().ToString(); + _controller = MockController.CreateController<$model$Controller>(new DataContext(_seed, DBTypeEnum.Memory), "user"); + } + + [TestMethod] + public void SearchTest() + { + PartialViewResult rv = (PartialViewResult)_controller.Index(); + Assert.IsInstanceOfType(rv.Model, typeof(IBasePagedListVM)); + string rv2 = _controller.Search((rv.Model as $model$ListVM).Searcher); + Assert.IsTrue(rv2.Contains("\"Code\":200")); + } + + [TestMethod] + public void CreateTest() + { + PartialViewResult rv = (PartialViewResult)_controller.Create(); + Assert.IsInstanceOfType(rv.Model, typeof($model$VM)); + + $model$VM vm = rv.Model as $model$VM; + $model$ v = new $model$(); + $cpros$ + vm.Entity = v; + _controller.Create(vm); + + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + var data = context.Set<$model$>().Find(v.ID); + $assert$ + Assert.AreEqual(data.CreateBy, "user"); + Assert.IsTrue(DateTime.Now.Subtract(data.CreateTime.Value).Seconds < 10); + } + + } + + [TestMethod] + public void EditTest() + { + $model$ v = new $model$(); + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + $pros$ + context.Set<$model$>().Add(v); + context.SaveChanges(); + } + + PartialViewResult rv = (PartialViewResult)_controller.Edit(v.ID.ToString()); + Assert.IsInstanceOfType(rv.Model, typeof($model$VM)); + + $model$VM vm = rv.Model as $model$VM; + vm.Wtm.DC = new DataContext(_seed, DBTypeEnum.Memory); + v = new $model$(); + v.ID = vm.Entity.ID; + $epros$ + vm.Entity = v; + vm.FC = new Dictionary(); + $fc$ + _controller.Edit(vm); + + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + var data = context.Set<$model$>().Find(v.ID); + $eassert$ + Assert.AreEqual(data.UpdateBy, "user"); + Assert.IsTrue(DateTime.Now.Subtract(data.UpdateTime.Value).Seconds < 10); + } + + } + + + [TestMethod] + public void DeleteTest() + { + $model$ v = new $model$(); + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + $pros$ + context.Set<$model$>().Add(v); + context.SaveChanges(); + } + + PartialViewResult rv = (PartialViewResult)_controller.Delete(v.ID.ToString()); + Assert.IsInstanceOfType(rv.Model, typeof($model$VM)); + + $model$VM vm = rv.Model as $model$VM; + v = new $model$(); + v.ID = vm.Entity.ID; + vm.Entity = v; + _controller.Delete(v.ID.ToString(),null); + + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + var data = context.Set<$model$>().Find(v.ID); + $del$ + } + + } + + + [TestMethod] + public void DetailsTest() + { + $model$ v = new $model$(); + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + $pros$ + context.Set<$model$>().Add(v); + context.SaveChanges(); + } + PartialViewResult rv = (PartialViewResult)_controller.Details(v.ID.ToString()); + Assert.IsInstanceOfType(rv.Model, typeof(IBaseCRUDVM)); + Assert.AreEqual(v.ID, (rv.Model as IBaseCRUDVM).Entity.GetID()); + } + + [TestMethod] + public void BatchEditTest() + { + $model$ v1 = new $model$(); + $model$ v2 = new $model$(); + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + $mpros$ + context.Set<$model$>().Add(v1); + context.Set<$model$>().Add(v2); + context.SaveChanges(); + } + + PartialViewResult rv = (PartialViewResult)_controller.BatchDelete(new string[] { v1.ID.ToString(), v2.ID.ToString() }); + Assert.IsInstanceOfType(rv.Model, typeof($model$BatchVM)); + + $model$BatchVM vm = rv.Model as $model$BatchVM; + vm.Ids = new string[] { v1.ID.ToString(), v2.ID.ToString() }; + $linkedpros$ + vm.FC = new Dictionary(); + $linkedfc$ + _controller.DoBatchEdit(vm, null); + + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + var data1 = context.Set<$model$>().Find(v1.ID); + var data2 = context.Set<$model$>().Find(v2.ID); + $meassert$ + Assert.AreEqual(data1.UpdateBy, "user"); + Assert.IsTrue(DateTime.Now.Subtract(data1.UpdateTime.Value).Seconds < 10); + Assert.AreEqual(data2.UpdateBy, "user"); + Assert.IsTrue(DateTime.Now.Subtract(data2.UpdateTime.Value).Seconds < 10); + } + } + + + [TestMethod] + public void BatchDeleteTest() + { + $model$ v1 = new $model$(); + $model$ v2 = new $model$(); + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + $mpros$ + context.Set<$model$>().Add(v1); + context.Set<$model$>().Add(v2); + context.SaveChanges(); + } + + PartialViewResult rv = (PartialViewResult)_controller.BatchDelete(new string[] { v1.ID.ToString(), v2.ID.ToString() }); + Assert.IsInstanceOfType(rv.Model, typeof($model$BatchVM)); + + $model$BatchVM vm = rv.Model as $model$BatchVM; + vm.Ids = new string[] { v1.ID.ToString(), v2.ID.ToString() }; + _controller.DoBatchDelete(vm, null); + + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + var data1 = context.Set<$model$>().Find(v1.ID); + var data2 = context.Set<$model$>().Find(v2.ID); + $mdel$ + } + } + + [TestMethod] + public void ExportTest() + { + PartialViewResult rv = (PartialViewResult)_controller.Index(); + Assert.IsInstanceOfType(rv.Model, typeof(IBasePagedListVM)); + IActionResult rv2 = _controller.ExportExcel(rv.Model as $model$ListVM); + Assert.IsTrue((rv2 as FileContentResult).FileContents.Length > 0); + } +$add$ + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/ControllerTestTopPoco.txt b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/ControllerTestTopPoco.txt new file mode 100644 index 0000000..adde414 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/ControllerTestTopPoco.txt @@ -0,0 +1,196 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using WalkingTec.Mvvm.Core; +using $cns$; +using $vns$; +using $mns$; +using $dns$; +$othernamespace$ + +namespace $tns$ +{ + [TestClass] + public class $model$ControllerTest + { + private $model$Controller _controller; + private string _seed; + + public $model$ControllerTest() + { + _seed = Guid.NewGuid().ToString(); + _controller = MockController.CreateController<$model$Controller>(new DataContext(_seed, DBTypeEnum.Memory), "user"); + } + + [TestMethod] + public void SearchTest() + { + PartialViewResult rv = (PartialViewResult)_controller.Index(); + Assert.IsInstanceOfType(rv.Model, typeof(IBasePagedListVM)); + string rv2 = _controller.Search((rv.Model as $model$ListVM).Searcher); + Assert.IsTrue(rv2.Contains("\"Code\":200")); + } + + [TestMethod] + public void CreateTest() + { + PartialViewResult rv = (PartialViewResult)_controller.Create(); + Assert.IsInstanceOfType(rv.Model, typeof($model$VM)); + + $model$VM vm = rv.Model as $model$VM; + $model$ v = new $model$(); + $cpros$ + vm.Entity = v; + _controller.Create(vm); + + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + var data = context.Set<$model$>().Find(v.ID); + $assert$ + } + + } + + [TestMethod] + public void EditTest() + { + $model$ v = new $model$(); + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + $pros$ + context.Set<$model$>().Add(v); + context.SaveChanges(); + } + + PartialViewResult rv = (PartialViewResult)_controller.Edit(v.ID.ToString()); + Assert.IsInstanceOfType(rv.Model, typeof($model$VM)); + + $model$VM vm = rv.Model as $model$VM; + vm.Wtm.DC = new DataContext(_seed, DBTypeEnum.Memory); + v = new $model$(); + v.ID = vm.Entity.ID; + $epros$ + vm.Entity = v; + vm.FC = new Dictionary(); + $fc$ + _controller.Edit(vm); + + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + var data = context.Set<$model$>().Find(v.ID); + $eassert$ + } + + } + + + [TestMethod] + public void DeleteTest() + { + $model$ v = new $model$(); + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + $pros$ + context.Set<$model$>().Add(v); + context.SaveChanges(); + } + + PartialViewResult rv = (PartialViewResult)_controller.Delete(v.ID.ToString()); + Assert.IsInstanceOfType(rv.Model, typeof($model$VM)); + + $model$VM vm = rv.Model as $model$VM; + v = new $model$(); + v.ID = vm.Entity.ID; + vm.Entity = v; + _controller.Delete(v.ID.ToString(),null); + + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + var data = context.Set<$model$>().Find(v.ID); + $del$ + } + + } + + + [TestMethod] + public void DetailsTest() + { + $model$ v = new $model$(); + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + $pros$ + context.Set<$model$>().Add(v); + context.SaveChanges(); + } + PartialViewResult rv = (PartialViewResult)_controller.Details(v.ID.ToString()); + Assert.IsInstanceOfType(rv.Model, typeof(IBaseCRUDVM)); + Assert.AreEqual(v.ID, (rv.Model as IBaseCRUDVM).Entity.GetID()); + } + + [TestMethod] + public void BatchEditTest() + { + $model$ v1 = new $model$(); + $model$ v2 = new $model$(); + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + $mpros$ + context.Set<$model$>().Add(v1); + context.Set<$model$>().Add(v2); + context.SaveChanges(); + } + + PartialViewResult rv = (PartialViewResult)_controller.BatchDelete(new string[] { v1.ID.ToString(), v2.ID.ToString() }); + Assert.IsInstanceOfType(rv.Model, typeof($model$BatchVM)); + + $model$BatchVM vm = rv.Model as $model$BatchVM; + vm.Ids = new string[] { v1.ID.ToString(), v2.ID.ToString() }; + $linkedpros$ + vm.FC = new Dictionary(); + $linkedfc$ + _controller.DoBatchEdit(vm, null); + + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + var data1 = context.Set<$model$>().Find(v1.ID); + var data2 = context.Set<$model$>().Find(v2.ID); + $meassert$ + } + } + + + [TestMethod] + public void BatchDeleteTest() + { + $model$ v1 = new $model$(); + $model$ v2 = new $model$(); + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + $mpros$ + context.Set<$model$>().Add(v1); + context.Set<$model$>().Add(v2); + context.SaveChanges(); + } + + PartialViewResult rv = (PartialViewResult)_controller.BatchDelete(new string[] { v1.ID.ToString(), v2.ID.ToString() }); + Assert.IsInstanceOfType(rv.Model, typeof($model$BatchVM)); + + $model$BatchVM vm = rv.Model as $model$BatchVM; + vm.Ids = new string[] { v1.ID.ToString(), v2.ID.ToString() }; + _controller.DoBatchDelete(vm, null); + + using (var context = new DataContext(_seed, DBTypeEnum.Memory)) + { + var data1 = context.Set<$model$>().Find(v1.ID); + var data2 = context.Set<$model$>().Find(v2.ID); + $mdel$ + } + } +$add$ + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/CrudVM.txt b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/CrudVM.txt new file mode 100644 index 0000000..d598392 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/CrudVM.txt @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.ComponentModel.DataAnnotations; +using WalkingTec.Mvvm.Core; +using WalkingTec.Mvvm.Core.Extensions; +using $modelnamespace$; +$othernamespace$ + +namespace $vmnamespace$ +{ + public partial class $classname$VM : BaseCRUDVM<$modelname$> + {$pros$ + + public $classname$VM() + {$include$ + } + + protected override void InitVM() + {$init$ + } + + public override void DoAdd() + {$add$ + base.DoAdd(); + } + + public override void DoEdit(bool updateAllFields = false) + {$edit$ + base.DoEdit(updateAllFields); + } + + public override void DoDelete() + { + base.DoDelete(); + } + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/HeaderFormat.txt b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/HeaderFormat.txt new file mode 100644 index 0000000..74399cf --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/HeaderFormat.txt @@ -0,0 +1,9 @@ + + private List $field$Format($classname$_View entity, object val) + { + return new List + { + ColumnFormatInfo.MakeDownloadButton(ButtonTypesEnum.Button,entity.$field$), + ColumnFormatInfo.MakeViewButton(ButtonTypesEnum.Button,entity.$field$,640,480), + }; + } diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/ImportVM.txt b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/ImportVM.txt new file mode 100644 index 0000000..ed39299 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/ImportVM.txt @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using WalkingTec.Mvvm.Core; +using WalkingTec.Mvvm.Core.Extensions; +using $modelnamespace$; +$othernamespace$ + +namespace $vmnamespace$ +{ + public partial class $classname$TemplateVM : BaseTemplateVM + {$pros$ + + protected override void InitVM() + {$init$ + } + + } + + public class $classname$ImportVM : BaseImportVM<$classname$TemplateVM, $modelname$> + { + + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/ListVM.txt b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/ListVM.txt new file mode 100644 index 0000000..a4fd746 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/ListVM.txt @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WalkingTec.Mvvm.Core; +using WalkingTec.Mvvm.Core.Extensions; +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using $modelnamespace$; +$othernamespace$ + +namespace $vmnamespace$ +{ + public partial class $classname$ListVM : BasePagedListVM<$classname$_View, $classname$Searcher> + {$actions$ + + protected override IEnumerable> InitGridHeader() + { + return new List>{$headers$ + this.MakeGridHeaderAction(width: 200) + }; + }$format$ + + public override IOrderedQueryable<$classname$_View> GetSearchQuery() + { + var query = DC.Set<$modelname$>()$where$ + .Select(x => new $classname$_View + { + ID = x.ID,$select$ + }) + .OrderBy(x => x.ID); + return query; + } + + } + + public class $classname$_View : $modelname${$subpros$ + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/BatchDeleteView.txt b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/BatchDeleteView.txt new file mode 100644 index 0000000..061492a --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/BatchDeleteView.txt @@ -0,0 +1,12 @@ +@model $vmnamespace$.$modelname$BatchVM +@inject IStringLocalizer Localizer; + + + @Localizer["Sys.BatchDeleteConfirm"] + + + + + + + diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/BatchEditView.txt b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/BatchEditView.txt new file mode 100644 index 0000000..1b95276 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/BatchEditView.txt @@ -0,0 +1,12 @@ +@model $vmnamespace$.$modelname$BatchVM +@inject IStringLocalizer Localizer; + + +
@Localizer["Sys.BatchEditConfirm"]
$fields$ + + + + + + +
diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/Controller.txt b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/Controller.txt new file mode 100644 index 0000000..4097c99 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/Controller.txt @@ -0,0 +1,219 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System; +using WalkingTec.Mvvm.Core; +using WalkingTec.Mvvm.Mvc; +using WalkingTec.Mvvm.Core.Extensions; +using $vmnamespace$; + +namespace $namespace$ +{ + $area$ + [ActionDescription("$des$")] + public partial class $modelname$Controller : BaseController + { + #region Search + [ActionDescription("Sys.Search")] + public ActionResult Index() + { + var vm = Wtm.CreateVM<$modelname$ListVM>(); + return PartialView(vm); + } + + [ActionDescription("Sys.Search")] + [HttpPost] + public string Search($modelname$Searcher searcher) + { + var vm = Wtm.CreateVM<$modelname$ListVM>(passInit: true); + if (ModelState.IsValid) + { + vm.Searcher = searcher; + return vm.GetJson(false); + } + else + { + return vm.GetError(); + } + } + + #endregion + + #region Create + [ActionDescription("Sys.Create")] + public ActionResult Create() + { + var vm = Wtm.CreateVM<$modelname$VM>(); + return PartialView(vm); + } + + [HttpPost] + [ActionDescription("Sys.Create")] + public ActionResult Create($modelname$VM vm) + { + if (!ModelState.IsValid) + { + return PartialView(vm); + } + else + { + vm.DoAdd(); + if (!ModelState.IsValid) + { + vm.DoReInit(); + return PartialView(vm); + } + else + { + return FFResult().CloseDialog().RefreshGrid(); + } + } + } + #endregion + + #region Edit + [ActionDescription("Sys.Edit")] + public ActionResult Edit(string id) + { + var vm = Wtm.CreateVM<$modelname$VM>(id); + return PartialView(vm); + } + + [ActionDescription("Sys.Edit")] + [HttpPost] + [ValidateFormItemOnly] + public ActionResult Edit($modelname$VM vm) + { + if (!ModelState.IsValid) + { + return PartialView(vm); + } + else + { + vm.DoEdit(); + if (!ModelState.IsValid) + { + vm.DoReInit(); + return PartialView(vm); + } + else + { + return FFResult().CloseDialog().RefreshGridRow(vm.Entity.ID); + } + } + } + #endregion + + #region Delete + [ActionDescription("Sys.Delete")] + public ActionResult Delete(string id) + { + var vm = Wtm.CreateVM<$modelname$VM>(id); + return PartialView(vm); + } + + [ActionDescription("Sys.Delete")] + [HttpPost] + public ActionResult Delete(string id, IFormCollection nouse) + { + var vm = Wtm.CreateVM<$modelname$VM>(id); + vm.DoDelete(); + if (!ModelState.IsValid) + { + return PartialView(vm); + } + else + { + return FFResult().CloseDialog().RefreshGrid(); + } + } + #endregion + + #region Details + [ActionDescription("Sys.Details")] + public ActionResult Details(string id) + { + var vm = Wtm.CreateVM<$modelname$VM>(id); + return PartialView(vm); + } + #endregion + + #region BatchEdit + [HttpPost] + [ActionDescription("Sys.BatchEdit")] + public ActionResult BatchEdit(string[] IDs) + { + var vm = Wtm.CreateVM<$modelname$BatchVM>(Ids: IDs); + return PartialView(vm); + } + + [HttpPost] + [ActionDescription("Sys.BatchEdit")] + public ActionResult DoBatchEdit($modelname$BatchVM vm, IFormCollection nouse) + { + if (!ModelState.IsValid || !vm.DoBatchEdit()) + { + return PartialView("BatchEdit",vm); + } + else + { + return FFResult().CloseDialog().RefreshGrid().Alert(Localizer["Sys.BatchEditSuccess", vm.Ids.Length]); + } + } + #endregion + + #region BatchDelete + [HttpPost] + [ActionDescription("Sys.BatchDelete")] + public ActionResult BatchDelete(string[] IDs) + { + var vm = Wtm.CreateVM<$modelname$BatchVM>(Ids: IDs); + return PartialView(vm); + } + + [HttpPost] + [ActionDescription("Sys.BatchDelete")] + public ActionResult DoBatchDelete($modelname$BatchVM vm, IFormCollection nouse) + { + if (!ModelState.IsValid || !vm.DoBatchDelete()) + { + return PartialView("BatchDelete",vm); + } + else + { + return FFResult().CloseDialog().RefreshGrid().Alert(Localizer["Sys.BatchDeleteSuccess", vm.Ids.Length]); + } + } + #endregion + + #region Import + [ActionDescription("Sys.Import")] + public ActionResult Import() + { + var vm = Wtm.CreateVM<$modelname$ImportVM>(); + return PartialView(vm); + } + + [HttpPost] + [ActionDescription("Sys.Import")] + public ActionResult Import($modelname$ImportVM vm, IFormCollection nouse) + { + if (vm.ErrorListVM.EntityList.Count > 0 || !vm.BatchSaveData()) + { + return PartialView(vm); + } + else + { + return FFResult().CloseDialog().RefreshGrid().Alert(Localizer["Sys.ImportSuccess", vm.EntityList.Count.ToString()]); + } + } + #endregion + + [ActionDescription("Sys.Export")] + [HttpPost] + public IActionResult ExportExcel($modelname$ListVM vm) + { + return vm.GetExportData(); + } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/CreateView.txt b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/CreateView.txt new file mode 100644 index 0000000..273bd2b --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/CreateView.txt @@ -0,0 +1,9 @@ +@model $vmnamespace$.$modelname$VM +@inject IStringLocalizer Localizer; + +$fields$ + + + + + diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/DeleteView.txt b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/DeleteView.txt new file mode 100644 index 0000000..4e70c53 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/DeleteView.txt @@ -0,0 +1,11 @@ +@model $vmnamespace$.$modelname$VM +@inject IStringLocalizer Localizer; + + + @Localizer["Sys.DeleteConfirm"]$fields$ + + + + + + diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/DetailsView.txt b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/DetailsView.txt new file mode 100644 index 0000000..0bee0df --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/DetailsView.txt @@ -0,0 +1,8 @@ +@model $vmnamespace$.$modelname$VM +@inject IStringLocalizer Localizer; + +$fields$ + + + + diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/EditView.txt b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/EditView.txt new file mode 100644 index 0000000..68e6c7d --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/EditView.txt @@ -0,0 +1,10 @@ +@model $vmnamespace$.$modelname$VM +@inject IStringLocalizer Localizer; + +$fields$ + + + + + + diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/ImportView.txt b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/ImportView.txt new file mode 100644 index 0000000..e68db4b --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/ImportView.txt @@ -0,0 +1,14 @@ +@model $vmnamespace$.$modelname$ImportVM +@inject IStringLocalizer Localizer; + + + + + + + + + + + + diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/ListView.txt b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/ListView.txt new file mode 100644 index 0000000..8142cd3 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Mvc/ListView.txt @@ -0,0 +1,6 @@ +@model $vmnamespace$.$modelname$ListVM +@inject IStringLocalizer Localizer; + +$fields$ + + diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Searcher.txt b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Searcher.txt new file mode 100644 index 0000000..dfbee55 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Searcher.txt @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using WalkingTec.Mvvm.Core; +using WalkingTec.Mvvm.Core.Extensions; +using $modelnamespace$; +$othernamespace$ + +namespace $vmnamespace$ +{ + public partial class $classname$Searcher : BaseSearcher + {$pros$ + + protected override void InitVM() + {$init$ + } + + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Blazor/Controller.txt b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Blazor/Controller.txt new file mode 100644 index 0000000..8d91ca1 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Blazor/Controller.txt @@ -0,0 +1,168 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Linq; +using WalkingTec.Mvvm.Core; +using WalkingTec.Mvvm.Core.Extensions; +using WalkingTec.Mvvm.Mvc; +using $vmnamespace$; +using $modelnamespace$; +$othernamespace$ + +namespace $namespace$ +{ + $area$ + $jwt$ + [ActionDescription("$des$")] + [ApiController] + [Route("api/$modelname$")] + public partial class $controllername$Controller : BaseApiController + { + [ActionDescription("Sys.Search")] + [HttpPost("Search")] + public IActionResult Search($controllername$Searcher searcher) + { + if (ModelState.IsValid) + { + var vm = Wtm.CreateVM<$controllername$ListVM>(); + vm.Searcher = searcher; + return Content(vm.GetJson(enumToString: false)); + } + else + { + return BadRequest(ModelState.GetErrorJson()); + } + } + + [ActionDescription("Sys.Get")] + [HttpGet("{id}")] + public $controllername$VM Get(string id) + { + var vm = Wtm.CreateVM<$controllername$VM>(id); + return vm; + } + + [ActionDescription("Sys.Create")] + [HttpPost("Add")] + public IActionResult Add($controllername$VM vm) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState.GetErrorJson()); + } + else + { + vm.DoAdd(); + if (!ModelState.IsValid) + { + return BadRequest(ModelState.GetErrorJson()); + } + else + { + return Ok(vm.Entity); + } + } + + } + + [ActionDescription("Sys.Edit")] + [HttpPut("Edit")] + public IActionResult Edit($controllername$VM vm) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState.GetErrorJson()); + } + else + { + vm.DoEdit(false); + if (!ModelState.IsValid) + { + return BadRequest(ModelState.GetErrorJson()); + } + else + { + return Ok(vm.Entity); + } + } + } + + [HttpPost("BatchDelete")] + [ActionDescription("Sys.Delete")] + public IActionResult BatchDelete(string[] ids) + { + var vm = Wtm.CreateVM<$controllername$BatchVM>(); + if (ids != null && ids.Count() > 0) + { + vm.Ids = ids; + } + else + { + return Ok(); + } + if (!ModelState.IsValid || !vm.DoBatchDelete()) + { + return BadRequest(ModelState.GetErrorJson()); + } + else + { + return Ok(ids.Count()); + } + } + + + [ActionDescription("Sys.Export")] + [HttpPost("ExportExcel")] + public IActionResult ExportExcel($controllername$Searcher searcher) + { + var vm = Wtm.CreateVM<$controllername$ListVM>(); + vm.Searcher = searcher; + vm.SearcherMode = ListVMSearchModeEnum.Export; + return vm.GetExportData(); + } + + [ActionDescription("Sys.CheckExport")] + [HttpPost("ExportExcelByIds")] + public IActionResult ExportExcelByIds(string[] ids) + { + var vm = Wtm.CreateVM<$controllername$ListVM>(); + if (ids != null && ids.Count() > 0) + { + vm.Ids = new List(ids); + vm.SearcherMode = ListVMSearchModeEnum.CheckExport; + } + return vm.GetExportData(); + } + + [ActionDescription("Sys.DownloadTemplate")] + [HttpGet("GetExcelTemplate")] + public IActionResult GetExcelTemplate() + { + var vm = Wtm.CreateVM<$controllername$ImportVM>(); + var qs = new Dictionary(); + foreach (var item in Request.Query.Keys) + { + qs.Add(item, Request.Query[item]); + } + vm.SetParms(qs); + var data = vm.GenerateTemplate(out string fileName); + return File(data, "application/vnd.ms-excel", fileName); + } + + [ActionDescription("Sys.Import")] + [HttpPost("Import")] + public ActionResult Import($controllername$ImportVM vm) + { + if (vm!=null && (vm.ErrorListVM.EntityList.Count > 0 || !vm.BatchSaveData())) + { + return BadRequest(vm.GetErrorJson()); + } + else + { + return Ok(vm?.EntityList?.Count ?? 0); + } + } + +$other$ + } +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Blazor/Create.txt b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Blazor/Create.txt new file mode 100644 index 0000000..83c1a4d --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Blazor/Create.txt @@ -0,0 +1,38 @@ +@page "$pagepath$" +@using $vmnamespace$; +@inherits BasePage + + + +$formfields$ + + + + +@code { + + private $modelname$VM Model = new $modelname$VM(); + private ValidateForm vform { get; set; } +$fieldinit$ + + protected override async Task OnInitializedAsync() + { +$init$ + await base.OnInitializedAsync(); + } + + + private async Task Submit(EditContext context) + { + await PostsForm(vform, "/api/$modelname$/add", (s) => "Sys.OprationSuccess"); + } + + public void OnClose() + { + CloseDialog(); + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Blazor/Details.txt b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Blazor/Details.txt new file mode 100644 index 0000000..1d49aea --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Blazor/Details.txt @@ -0,0 +1,34 @@ +@page "$pagepath$" +@using $vmnamespace$; +@inherits BasePage + + + +$formfields$ + + + + +@code { + + private $modelname$VM Model = null; + private ValidateForm vform { get; set; } + [Parameter] + public string id { get; set; } +$fieldinit$ + + protected override async Task OnInitializedAsync() + { +$init$ + var rv = await WtmBlazor.Api.CallAPI<$modelname$VM>($"/api/$modelname$/{id}"); + Model = rv.Data; + } + + public void OnClose() + { + CloseDialog(); + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Blazor/Edit.txt b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Blazor/Edit.txt new file mode 100644 index 0000000..4956e2f --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Blazor/Edit.txt @@ -0,0 +1,41 @@ +@page "$pagepath$" +@using $vmnamespace$; +@inherits BasePage + + + +$formfields$ + + + + +@code { + + private $modelname$VM Model = null; + private ValidateForm vform { get; set; } + [Parameter] + public string id { get; set; } +$fieldinit$ + + protected override async Task OnInitializedAsync() + { +$init$ + var rv = await WtmBlazor.Api.CallAPI<$modelname$VM>($"/api/$modelname$/{id}"); + Model = rv.Data; + await base.OnInitializedAsync(); + } + + private async Task Submit(EditContext context) + { + await PostsForm(vform, $"/api/$modelname$/edit", (s) => "Sys.OprationSuccess", method: HttpMethodEnum.PUT); + } + + public void OnClose() + { + CloseDialog(); + } + +} diff --git a/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Blazor/Import.txt b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Blazor/Import.txt new file mode 100644 index 0000000..8785197 --- /dev/null +++ b/WalkingTec.Mvvm/WalkingTec.Mvvm.Mvc/GeneratorFiles/Spa/Blazor/Import.txt @@ -0,0 +1,66 @@ +@page "$pagepath$" +@using $vmnamespace$; +@inherits BasePage + + + + 1@WtmBlazor.Localizer["Sys.ImportStep1"] +